1+ /**
2+ * OIDC Relying Party API handler module.
3+ */
4+
5+ import express from 'express'
6+ import { routeResolvedFile } from '../../utils.mjs'
7+ import { urlencoded } from 'body-parser'
8+ const bodyParser = urlencoded ( { extended : false } )
9+ import OidcManager from '../../models/oidc-manager.mjs'
10+ import { LoginRequest } from '../../requests/login-request.mjs'
11+ import { SharingRequest } from '../../requests/sharing-request.mjs'
12+
13+ import restrictToTopDomain from '../../handlers/restrict-to-top-domain.mjs'
14+
15+ import PasswordResetEmailRequest from '../../requests/password-reset-email-request.mjs'
16+ import PasswordChangeRequest from '../../requests/password-change-request.mjs'
17+
18+ import { createRequire } from 'module'
19+ const require = createRequire ( import . meta. url )
20+ const { AuthCallbackRequest } = require ( '@solid/oidc-auth-manager' ) . handlers
21+
22+ /**
23+ * Sets up OIDC authentication for the given app.
24+ *
25+ * @param app {Object} Express.js app instance
26+ * @param argv {Object} Config options hashmap
27+ */
28+ export function initialize ( app , argv ) {
29+ const oidc = OidcManager . fromServerConfig ( argv )
30+ app . locals . oidc = oidc
31+ oidc . initialize ( )
32+
33+ // Attach the OIDC API
34+ app . use ( '/' , middleware ( oidc ) )
35+
36+ // Perform the actual authentication
37+ app . use ( '/' , async ( req , res , next ) => {
38+ oidc . rs . authenticate ( { tokenTypesSupported : argv . tokenTypesSupported } ) ( req , res , ( err ) => {
39+ // Error handling should be deferred to the ldp in case a user with a bad token is trying
40+ // to access a public resource
41+ if ( err ) {
42+ req . authError = err
43+ res . status ( 200 )
44+ }
45+ next ( )
46+ } )
47+ } )
48+
49+ // Expose session.userId
50+ app . use ( '/' , ( req , res , next ) => {
51+ oidc . webIdFromClaims ( req . claims )
52+ . then ( webId => {
53+ if ( webId ) {
54+ req . session . userId = webId
55+ }
56+
57+ next ( )
58+ } )
59+ . catch ( err => {
60+ const error = new Error ( 'Could not verify Web ID from token claims' )
61+ error . statusCode = 401
62+ error . statusText = 'Invalid login'
63+ error . cause = err
64+
65+ console . error ( err )
66+
67+ next ( error )
68+ } )
69+ } )
70+ }
71+
72+ /**
73+ * Returns a router with OIDC Relying Party and Identity Provider middleware:
74+ *
75+ * @method middleware
76+ *
77+ * @param oidc {OidcManager}
78+ *
79+ * @return {Router } Express router
80+ */
81+ export function middleware ( oidc ) {
82+ const router = express . Router ( '/' )
83+
84+ // User-facing Authentication API
85+ router . get ( [ '/login' , '/signin' ] , LoginRequest . get )
86+
87+ router . post ( '/login/password' , bodyParser , LoginRequest . loginPassword )
88+
89+ router . post ( '/login/tls' , bodyParser , LoginRequest . loginTls )
90+
91+ router . get ( '/sharing' , SharingRequest . get )
92+ router . post ( '/sharing' , bodyParser , SharingRequest . share )
93+
94+ router . get ( '/account/password/reset' , restrictToTopDomain , PasswordResetEmailRequest . get )
95+ router . post ( '/account/password/reset' , restrictToTopDomain , bodyParser , PasswordResetEmailRequest . post )
96+
97+ router . get ( '/account/password/change' , restrictToTopDomain , PasswordChangeRequest . get )
98+ router . post ( '/account/password/change' , restrictToTopDomain , bodyParser , PasswordChangeRequest . post )
99+
100+ router . get ( '/.well-known/solid/logout/' , ( req , res ) => res . redirect ( '/logout' ) )
101+
102+ router . get ( '/goodbye' , ( req , res ) => { res . render ( 'auth/goodbye' ) } )
103+
104+ // The relying party callback is called at the end of the OIDC signin process
105+ router . get ( '/api/oidc/rp/:issuer_id' , AuthCallbackRequest . get )
106+
107+ // Static assets related to authentication
108+ const authAssets = [
109+ [ '/.well-known/solid/login/' , '../static/popup-redirect.html' , false ] ,
110+ [ '/common/' , 'solid-auth-client/dist-popup/popup.html' ]
111+ ]
112+ authAssets . map ( args => routeResolvedFile ( router , ...args ) )
113+
114+ // Initialize the OIDC Identity Provider routes/api
115+ // router.get('/.well-known/openid-configuration', discover.bind(provider))
116+ // router.get('/jwks', jwks.bind(provider))
117+ // router.post('/register', register.bind(provider))
118+ // router.get('/authorize', authorize.bind(provider))
119+ // router.post('/authorize', authorize.bind(provider))
120+ // router.post('/token', token.bind(provider))
121+ // router.get('/userinfo', userinfo.bind(provider))
122+ // router.get('/logout', logout.bind(provider))
123+ const oidcProviderApi = require ( 'oidc-op-express' ) ( oidc . provider )
124+ router . use ( '/' , oidcProviderApi )
125+
126+ return router
127+ }
128+
129+ /**
130+ * Sets the `WWW-Authenticate` response header for 401 error responses.
131+ * Used by error-pages handler.
132+ *
133+ * @param req {IncomingRequest}
134+ * @param res {ServerResponse}
135+ * @param err {Error}
136+ */
137+ export function setAuthenticateHeader ( req , res , err ) {
138+ const locals = req . app . locals
139+
140+ const errorParams = {
141+ realm : locals . host . serverUri ,
142+ scope : 'openid webid' ,
143+ error : err . error ,
144+ error_description : err . error_description ,
145+ error_uri : err . error_uri
146+ }
147+
148+ const challengeParams = Object . keys ( errorParams )
149+ . filter ( key => ! ! errorParams [ key ] )
150+ . map ( key => `${ key } ="${ errorParams [ key ] } "` )
151+ . join ( ', ' )
152+
153+ res . set ( 'WWW-Authenticate' , 'Bearer ' + challengeParams )
154+ }
155+
156+ /**
157+ * Provides custom logic for error status code overrides.
158+ *
159+ * @param statusCode {number}
160+ * @param req {IncomingRequest}
161+ *
162+ * @returns {number }
163+ */
164+ export function statusCodeOverride ( statusCode , req ) {
165+ if ( isEmptyToken ( req ) ) {
166+ return 400
167+ } else {
168+ return statusCode
169+ }
170+ }
171+
172+ /**
173+ * Tests whether the `Authorization:` header includes an empty or missing Bearer
174+ * token.
175+ *
176+ * @param req {IncomingRequest}
177+ *
178+ * @returns {boolean }
179+ */
180+ export function isEmptyToken ( req ) {
181+ const header = req . get ( 'Authorization' )
182+
183+ if ( ! header ) { return false }
184+
185+ if ( header . startsWith ( 'Bearer' ) ) {
186+ const fragments = header . split ( ' ' )
187+
188+ if ( fragments . length === 1 ) {
189+ return true
190+ } else if ( ! fragments [ 1 ] ) {
191+ return true
192+ }
193+ }
194+
195+ return false
196+ }
197+
198+ export default {
199+ initialize,
200+ isEmptyToken,
201+ middleware,
202+ setAuthenticateHeader,
203+ statusCodeOverride
204+ }
0 commit comments