@@ -3,49 +3,69 @@ import svgCaptcha from 'svg-captcha'
33import { Authenticator } from 'passport'
44import { Strategy as BearerStrategy } from 'passport-http-bearer'
55import { defaultHashUtil } from '../utilities/password-hashing'
6- import { JWTService , Payload , TokenVerificationError , VerificationErrorReason } from './verification'
6+ import { JWTService , Payload , TokenVerificationError , VerificationErrorReason , TokenAssertion } from './verification'
77import { invalidInput , InvalidInputError , MageError } from '../app.api/app.api.errors'
88import { IdentityProviderRepository } from './ingress.entities'
99import { AdmitFromIdentityProviderOperation , EnrollMyselfOperation , EnrollMyselfRequest } from './ingress.app.api'
1010import { IngressProtocolWebBinding } from './ingress.protocol.bindings'
1111
1212declare module 'express-serve-static-core' {
1313 interface Request {
14- identityProviderService ?: IngressProtocolWebBinding
14+ ingress ?: IngressRequestContext
1515 }
1616}
1717
18+ type IngressRequestContext = { identityProviderService : IngressProtocolWebBinding } & (
19+ | { state : 'init' }
20+ | { state : 'localEnrollment' , localEnrollment : LocalEnrollment }
21+ )
22+
23+ type LocalEnrollment =
24+ | {
25+ state : 'humanTokenVerified'
26+ captchaTokenPayload : Payload
27+ }
28+ | {
29+ state : 'humanVerified'
30+ subject : string
31+ }
32+
1833export type IngressOperations = {
1934 enrollMyself : EnrollMyselfOperation
2035 admitFromIdentityProvider : AdmitFromIdentityProviderOperation
2136}
2237
38+ export type IngressRoutes = {
39+ localEnrollment : express . Router
40+ idpAdmission : express . Router
41+ }
42+
2343function bindingFor ( idpName : string ) : IngressProtocolWebBinding {
2444 throw new Error ( 'unimplemented' )
2545}
2646
27- export function CreateIngressRoutes ( ingressApp : IngressOperations , idpRepo : IdentityProviderRepository , tokenService : JWTService , passport : Authenticator ) : express . Router {
47+ export function CreateIngressRoutes ( ingressApp : IngressOperations , idpRepo : IdentityProviderRepository , tokenService : JWTService , passport : Authenticator ) : IngressRoutes {
2848
2949 const captchaBearer = new BearerStrategy ( ( token , done ) => {
3050 const expectation = {
3151 subject : null ,
3252 expiration : null ,
33- assertion : TokenAssertion . Captcha
53+ assertion : TokenAssertion . IsHuman
3454 }
3555 tokenService . verifyToken ( token , expectation )
3656 . then ( payload => done ( null , payload ) )
3757 . catch ( err => done ( err ) )
3858 } )
3959
40- const routes = express . Router ( )
41-
42- // TODO: signup
43- // TODO: signin
60+ // TODO: separate routers for /auth/idp/* and /api/users/signups/* for backward compatibility
4461
4562 const routeToIdp = express . Router ( ) . all ( '/' ,
4663 ( ( req , res , next ) => {
47- const idpService = req . identityProviderService !
48- idpService . handleRequest ( req , res , next )
64+ const idpService = req . ingress ?. identityProviderService
65+ if ( idpService ) {
66+ return idpService . handleRequest ( req , res , next )
67+ }
68+ next ( new Error ( `no identity provider for ingress request: ${ req . method } ${ req . originalUrl } ` ) )
4969 } ) as express . RequestHandler ,
5070 ( async ( err , req , res , next ) => {
5171 if ( err ) {
@@ -56,34 +76,53 @@ export function CreateIngressRoutes(ingressApp: IngressOperations, idpRepo: Iden
5676 console . error ( 'unexpected authentication user type:' , req . user ?. from )
5777 return res . status ( 500 ) . send ( 'unexpected authentication result' )
5878 }
59- const identityProviderName = req . identityProviderService ! . idp . name
79+ const identityProviderName = req . ingress ! . identityProviderService ! . idp . name
6080 const identityProviderUser = req . user . account
61- const ingressResult = await ingressApp . admitFromIdentityProvider ( { identityProviderName, identityProviderUser } )
62- if ( ingressResult . error ) {
63- next ( ingressResult . error )
81+ const admission = await ingressApp . admitFromIdentityProvider ( { identityProviderName, identityProviderUser } )
82+ if ( admission . error ) {
83+ return next ( admission . error )
84+ }
85+ const { admissionToken, mageAccount } = admission . success
86+ /*
87+ TODO: copied from redirecting protocols - cleanup and adapt here
88+ local/ldap use direct json response
89+ saml uses RelayState body property
90+ oauth/oidc use state query parameter
91+ can all use direct json response and handle redirect windows client side?
92+ */
93+ if ( req . query . state === 'mobile' ) {
94+ let uri ;
95+ if ( ! mageAccount . active || ! mageAccount . enabled ) {
96+ uri = `mage://app/invalid_account?active=${ mageAccount . active } &enabled=${ mageAccount . enabled } ` ;
97+ } else {
98+ uri = `mage://app/authentication?token=${ req . token } `
99+ }
100+ res . redirect ( uri ) ;
101+ } else {
102+ res . render ( 'authentication' , { host : req . getRoot ( ) , success : true , login : { token : req . token , user : req . user } } ) ;
64103 }
65- // if user active and enabled, send authenticated JWT and proceed to verification
66- // else
67- const account = ingressResult . success !
68-
69104 } ) as express . ErrorRequestHandler
70105 )
71106
72- routes . use ( '/:identityProviderName' ,
107+ // TODO: mount to /auth
108+ const idpAdmission = express . Router ( )
109+ idpAdmission . use ( '/:identityProviderName' ,
73110 ( req , res , next ) => {
74111 const idpName = req . params . identityProviderName
75112 const idpService = bindingFor ( idpName )
76113 if ( idpService ) {
77- req . identityProviderService = idpService
114+ req . ingress = { state : 'init' , identityProviderService : idpService }
78115 return next ( )
79116 }
80117 res . status ( 404 ) . send ( `${ idpName } not found` )
81118 } ,
119+ // use a sub-router so express implicitly strips the base url /auth/:identityProviderName before routing idp handler
82120 routeToIdp
83121 )
84122
85123 // TODO: mount to /api/users/signups
86- routes . route ( '/signups' )
124+ const localEnrollment = express . Router ( )
125+ localEnrollment . route ( '/signups' )
87126 . post ( async ( req , res , next ) => {
88127 try {
89128 const username = typeof req . body . username === 'string' ? req . body . username . trim ( ) : null
@@ -99,7 +138,7 @@ export function CreateIngressRoutes(ingressApp: IngressOperations, idpRepo: Iden
99138 } )
100139 const captchaHash = await defaultHashUtil . hashPassword ( captcha . text )
101140 const claims = { captcha : captchaHash }
102- const verificationToken = await tokenService . generateToken ( username , TokenAssertion . Captcha , 60 * 3 , claims )
141+ const verificationToken = await tokenService . generateToken ( username , TokenAssertion . IsHuman , 60 * 3 , claims )
103142 res . json ( {
104143 token : verificationToken ,
105144 captcha : `data:image/svg+xml;base64,${ Buffer . from ( captcha . data ) . toString ( 'base64' ) } `
@@ -110,7 +149,8 @@ export function CreateIngressRoutes(ingressApp: IngressOperations, idpRepo: Iden
110149 }
111150 } )
112151
113- routes . route ( '/signups/verifications' )
152+ // TODO: mount to /api/users/signups/verifications
153+ localEnrollment . route ( '/signups/verifications' )
114154 . post (
115155 async ( req , res , next ) => {
116156 passport . authenticate ( captchaBearer , ( err : TokenVerificationError , captchaTokenPayload : Payload ) => {
@@ -123,18 +163,26 @@ export function CreateIngressRoutes(ingressApp: IngressOperations, idpRepo: Iden
123163 if ( ! captchaTokenPayload ) {
124164 return res . status ( 400 ) . send ( 'Missing captcha token' )
125165 }
126- req . user = captchaTokenPayload
166+ req . ingress = {
167+ ...req . ingress ! ,
168+ state : 'localEnrollment' ,
169+ localEnrollment : { state : 'humanTokenVerified' , captchaTokenPayload } }
127170 next ( )
128171 } ) ( req , res , next )
129172 } ,
130173 async ( req , res , next ) => {
131174 try {
132- const isHuman = await defaultHashUtil . validPassword ( req . body . captchaText , req . user . captcha )
175+ if ( req . ingress ?. state !== 'localEnrollment' || req . ingress . localEnrollment . state !== 'humanTokenVerified' ) {
176+ return res . status ( 500 ) . send ( 'invalid ingress state' )
177+ }
178+ const tokenPayload = req . ingress . localEnrollment . captchaTokenPayload
179+ const hashedCaptchaText = tokenPayload . captcha as string
180+ const userCaptchaText = req . body . captchaText
181+ const isHuman = await defaultHashUtil . validPassword ( userCaptchaText , hashedCaptchaText )
133182 if ( ! isHuman ) {
134183 return res . status ( 403 ) . send ( 'Invalid captcha. Please try again.' )
135184 }
136- const payload = req . user as Payload
137- const username = payload . subject !
185+ const username = tokenPayload . subject !
138186 const parsedEnrollment = validateEnrollment ( req . body )
139187 if ( parsedEnrollment instanceof MageError ) {
140188 return next ( parsedEnrollment )
@@ -155,15 +203,15 @@ export function CreateIngressRoutes(ingressApp: IngressOperations, idpRepo: Iden
155203 }
156204 )
157205
158- return routes
206+ return { localEnrollment , idpAdmission }
159207}
160208
161209function validateEnrollment ( input : any ) : Omit < EnrollMyselfRequest , 'username' > | InvalidInputError {
162210 const { displayName, email, password, phone } = input
163- if ( ! displayName ) {
211+ if ( typeof displayName !== 'string' ) {
164212 return invalidInput ( 'displayName is required' )
165213 }
166- if ( ! password ) {
214+ if ( typeof password !== 'string' ) {
167215 return invalidInput ( 'password is required' )
168216 }
169217 const enrollment : Omit < EnrollMyselfRequest , 'username' > = { displayName, password }
0 commit comments