Skip to content

Commit e349a05

Browse files
WIP
1 parent f1a62f3 commit e349a05

File tree

10 files changed

+369
-2
lines changed

10 files changed

+369
-2
lines changed

lib/create-app.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ var proxy = require('./handlers/proxy')
1010
var IdentityProvider = require('./identity-provider')
1111
var vhost = require('vhost')
1212
var path = require('path')
13+
var AnvilConnect = require('./handlers/auth-oidc')
14+
var bodyParser = require('body-parser')
15+
1316
var corsSettings = cors({
1417
methods: [
1518
'OPTIONS', 'HEAD', 'GET', 'PATCH', 'POST', 'PUT', 'DELETE'
@@ -75,6 +78,13 @@ function createApp (argv) {
7578
app.use('/', corsSettings, idp.get.bind(idp))
7679
}
7780

81+
ldp.oidc = true
82+
if (ldp.oidc) {
83+
var oidc = OidcProvider()
84+
app.use('/', oidc.authenticate())
85+
app.use('/api/oidc', oidc.middleware(corsSettings))
86+
}
87+
7888
if (ldp.idp) {
7989
app.use(vhost('*', LdpMiddleware(corsSettings)))
8090
}

lib/handlers/auth-oidc.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* Module dependencies
3+
*/
4+
5+
var AnvilConnect = require('anvil-connect-nodejs')
6+
var UnauthorizedError = AnvilConnect.UnauthorizedError
7+
var debug = require('debug')('ldnode:auth-oidc')
8+
9+
/**
10+
* Constructor
11+
*/
12+
13+
function AnvilConnectExpress (options) {
14+
options = options || {}
15+
this.client = new AnvilConnect(options)
16+
this.respond = options.respond || true
17+
}
18+
19+
/**
20+
* Next
21+
*/
22+
23+
function handler (req, res, next) {
24+
var self = this
25+
26+
return function (err) {
27+
var statusCode = err.statusCode || 400
28+
29+
if (self.respond) {
30+
res.status(statusCode).json({
31+
error: err.error,
32+
error_description: err.error_description
33+
})
34+
} else {
35+
next(err)
36+
}
37+
}
38+
}
39+
40+
AnvilConnectExpress.prototype.handler = handler
41+
42+
/**
43+
* Verify Middleware
44+
*/
45+
46+
function verifier (options) {
47+
var self = this
48+
49+
return function (req, res, next) {
50+
var accessToken
51+
var nexter = self.handler(req, res, next)
52+
53+
// Check for an access token in the Authorization header
54+
if (req.headers && req.headers.authorization) {
55+
var components = req.headers.authorization.split(' ')
56+
var scheme = components[0]
57+
var credentials = components[1]
58+
59+
if (components.length !== 2) {
60+
return nexter(new UnauthorizedError({
61+
error: 'invalid_request',
62+
error_description: 'Invalid authorization header',
63+
statusCode: 400
64+
}))
65+
}
66+
67+
if (scheme !== 'Bearer') {
68+
return nexter(new UnauthorizedError({
69+
error: 'invalid_request',
70+
error_description: 'Invalid authorization scheme',
71+
statusCode: 400
72+
}))
73+
}
74+
75+
accessToken = credentials
76+
}
77+
78+
// Check for an access token in the request URI
79+
if (req.query && req.query.access_token) {
80+
if (accessToken) {
81+
return nexter(new UnauthorizedError({
82+
error: 'invalid_request',
83+
error_description: 'Multiple authentication methods',
84+
statusCode: 400
85+
}))
86+
}
87+
88+
accessToken = req.query.access_token
89+
}
90+
91+
// Check for an access token in the request body
92+
if (req.body && req.body.access_token) {
93+
if (accessToken) {
94+
return nexter(new UnauthorizedError({
95+
error: 'invalid_request',
96+
error_description: 'Multiple authentication methods',
97+
statusCode: 400
98+
}))
99+
}
100+
101+
if (req.headers &&
102+
req.headers['content-type'] !== 'application/x-www-form-urlencoded') {
103+
return nexter(new UnauthorizedError({
104+
error: 'invalid_request',
105+
error_description: 'Invalid content-type',
106+
statusCode: 400
107+
}))
108+
}
109+
110+
accessToken = req.body.access_token
111+
}
112+
113+
function invokeVerification () {
114+
self.client.verify(accessToken, options)
115+
.then(function (accessTokenClaims) {
116+
req.accessToken = accessToken
117+
req.accessTokenClaims = accessTokenClaims
118+
return self.client.userInfo({token: accessToken})
119+
})
120+
.then(function (userInfo) {
121+
req.userInfo = userInfo
122+
req.session.userId = userInfo.profile
123+
req.session.identified = true
124+
next()
125+
})
126+
.catch(function (err) {
127+
nexter(err)
128+
})
129+
}
130+
131+
// Missing access token
132+
if (!accessToken) {
133+
debug('No authentication token!')
134+
next()
135+
// return nexter(new UnauthorizedError({
136+
// realm: 'user',
137+
// error: 'invalid_request',
138+
// error_description: 'An access token is required',
139+
// statusCode: 400
140+
// }))
141+
142+
// Access token found
143+
} else {
144+
// If JWKs are not set, attempt to retrieve them first
145+
if (!self.client.jwks) {
146+
self.client.discover().then(function () {
147+
return self.client.getJWKs()
148+
})
149+
// then verify the token and carry on
150+
.then(invokeVerification)
151+
.catch(function (err) {
152+
nexter(err)
153+
})
154+
// otherwise, verify the token right away
155+
} else {
156+
invokeVerification()
157+
}
158+
}
159+
}
160+
}
161+
AnvilConnectExpress.prototype.verifier = verifier
162+
163+
var url = require('url')
164+
function fullUrl(req) {
165+
return url.format({
166+
protocol: req.protocol,
167+
host: req.get('host'),
168+
pathname: req.originalUrl
169+
})
170+
}
171+
172+
function urlForLogin (req) {
173+
// return 'https://anvil.local/authorize?stuff'
174+
var loginUrl = this.client.authorizationUri({
175+
endpoint: 'signin',
176+
nonce: '123',
177+
response_mode: 'query',
178+
response_type: 'token id_token',
179+
redirect_uri: 'https://ldnode.local:8443/rp'
180+
})
181+
return loginUrl
182+
}
183+
AnvilConnectExpress.prototype.urlForLogin = urlForLogin
184+
185+
186+
/**
187+
* Export
188+
*/
189+
190+
module.exports = AnvilConnectExpress

lib/handlers/auth-rp.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = handler
2+
3+
function handler (req, res, next) {
4+
if (req.session.returnToUrl) {
5+
res.redirect(302, req.session.returnToUrl)
6+
}
7+
}

lib/handlers/error-pages.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = handler
22

33
var debug = require('../debug').server
44
var fs = require('fs')
5+
var util = require('../utils')
56

67
function handler (err, req, res, next) {
78
debug('Error page because of ' + err.message)
@@ -17,9 +18,17 @@ function handler (err, req, res, next) {
1718
// If noErrorPages is set,
1819
// then use built-in express default error handler
1920
if (ldp.noErrorPages) {
20-
return res
21+
22+
if (err.status === 401 && req.accepts('text/html')) {
23+
res.status(err.status)
24+
redirectToLogin(err, req, res, next)
25+
return
26+
}
27+
28+
res
2129
.status(err.status)
2230
.send(err.message + '\n' || '')
31+
return
2332
}
2433

2534
// Check if error page exists
@@ -36,3 +45,28 @@ function handler (err, req, res, next) {
3645
res.send(text)
3746
})
3847
}
48+
49+
function redirectBody (url) {
50+
return `<!DOCTYPE HTML>
51+
<meta charset="UTF-8">
52+
<script>
53+
window.location.href = "${url}"
54+
</script>
55+
<noscript>
56+
<meta http-equiv="refresh" content="0; url=${url}">
57+
</noscript>
58+
<title>Redirecting...</title>
59+
If you are not redirected automatically, follow the <a href='${url}'>link to login</a>
60+
`
61+
}
62+
63+
function redirectToLogin (err, req, res, next) {
64+
res.header('Content-Type', 'text/html')
65+
var loginUrl = req.app.locals.oidcClient.urlForLogin(req)
66+
debug('Redirecting to login: ' + loginUrl)
67+
var currentUrl = util.fullUrlForReq(req)
68+
req.session.returnToUrl = currentUrl
69+
70+
var body = redirectBody(loginUrl)
71+
res.send(body)
72+
}

lib/ldp-middleware.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function LdpMiddleware (corsSettings) {
2121
router.use(corsSettings)
2222
}
2323

24-
router.use('/*', authentication)
24+
// router.use('/*', authentication)
2525
router.get('/*', index, acl.allow('Read'), get)
2626
router.post('/*', acl.allow('Append'), post)
2727
router.patch('/*', acl.allow('Append'), patch)

lib/oidc-provider.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Configure a client for the local OpenID Connect Provider service
2+
// var oidc =
3+
// app.locals.oidcClient = oidc
4+
// oidc.client.discover()
5+
6+
const bodyParser = require('body-parser')
7+
const Store = require('abstract-oidc-store')
8+
const authRp = require('./handlers/auth-rp')
9+
10+
module.exports = Provider
11+
// path.join(
12+
var rpPath = '/api/oidc/rp'
13+
14+
function getIssuer (token) {
15+
const claims = jwtDecode(token)
16+
if (!claims) return claims.iss
17+
return false
18+
}
19+
20+
class Provider {
21+
constructor (options = {}) {
22+
this.clients = options.store || new Store()
23+
this.registration = options.registration
24+
}
25+
26+
authenticate (req, res, next) {
27+
const token = getToken(req)
28+
// get issuer from token
29+
const issuer = getIssuer(token)
30+
// retrieve it from store
31+
this.clients.get(issuer)
32+
.then((client) => {
33+
if (client) {
34+
return client
35+
}
36+
var client = new AnvilConnect({
37+
issuer: issuer,
38+
redirect_uri: redirectUri,
39+
scope: 'openid profile'
40+
})
41+
42+
return client.discover()
43+
.then(() => client.register(this.registration))
44+
.then(() => this.clients.put(client))
45+
.then(() => client)
46+
})
47+
.then((client) => client.verify()(req, res, next))
48+
}
49+
50+
middleware (corsSetting) {
51+
const router = express().Router('/')
52+
53+
if (corsSettings) {
54+
router.use(corsSettings)
55+
}
56+
57+
router.use('/oidc', express.static(path.join(__dirname, '../static/oidc')))
58+
router.post('/oidc/signin', bodyParser.urlencoded({extended: false}),
59+
(req, res, next) => {
60+
const userServer = req.body.oidcServer
61+
})
62+
router.get('/rp', oidc.verifier(), authRp)
63+
router.get('/signout', (req, res, next) => {
64+
req.session.userId = null
65+
req.session.identified = false
66+
res.send('signed out...')
67+
})
68+
69+
return router
70+
}
71+
}
72+
73+
var oidcMap = {
74+
'ldnode.local': new AnvilConnect({
75+
issuer: 'https://anvil.local',
76+
client_id: 'cfe8d9a7-e1f6-4b88-9d55-0be004a62870',
77+
client_secret: '62b0bc1698ff97bdc7c7',
78+
redirect_uri: 'https://ldnode.local:8443/api/oidc/rp'
79+
})
80+
}

lib/utils.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,22 @@ exports.serialize = serialize
1111
exports.translate = translate
1212
exports.stringToStream = stringToStream
1313
exports.reqToPath = reqToPath
14+
exports.fullUrlForReq = fullUrlForReq
1415

1516
var fs = require('fs')
1617
var path = require('path')
1718
var S = require('string')
1819
var $rdf = require('rdflib')
1920
var from = require('from2')
21+
var url = require('url')
22+
23+
function fullUrlForReq(req) {
24+
return url.format({
25+
protocol: req.protocol,
26+
host: req.get('host'),
27+
pathname: req.originalUrl
28+
})
29+
}
2030

2131
function uriToFilename (uri, base) {
2232
uri = decodeURIComponent(uri)

0 commit comments

Comments
 (0)