@@ -4,23 +4,31 @@ import { Authenticator } from 'passport'
44import { Strategy as BearerStrategy } from 'passport-http-bearer'
55import { defaultHashUtil } from '../utilities/password-hashing'
66import { JWTService , Payload , TokenVerificationError , VerificationErrorReason , TokenAssertion } from './verification'
7- import { invalidInput , InvalidInputError , MageError } from '../app.api/app.api.errors'
8- import { IdentityProviderRepository } from './ingress.entities'
7+ import { invalidInput , InvalidInputError , MageError , permissionDenied } from '../app.api/app.api.errors'
8+ import { IdentityProvider , IdentityProviderRepository } from './ingress.entities'
99import { AdmitFromIdentityProviderOperation , EnrollMyselfOperation , EnrollMyselfRequest } from './ingress.app.api'
10- import { IngressProtocolWebBinding } from './ingress.protocol.bindings'
10+ import { IngressProtocolWebBinding , IngressResponseType } from './ingress.protocol.bindings'
11+ import { createWebBinding as LocalBinding } from './ingress.protocol.local'
12+ import { createWebBinding as OAuthBinding } from './ingress.protocol.oauth'
1113
1214declare module 'express-serve-static-core' {
1315 interface Request {
1416 ingress ?: IngressRequestContext
17+ localEnrollment ?: LocalEnrollmentContext
1518 }
1619}
1720
18- type IngressRequestContext = { identityProviderService : IngressProtocolWebBinding } & (
19- | { state : 'init' }
20- | { state : 'localEnrollment' , localEnrollment : LocalEnrollment }
21- )
21+ enum UserAgentType {
22+ MobileApp = 'MobileApp' ,
23+ WebApp = 'WebApp'
24+ }
25+
26+ type IngressRequestContext = {
27+ idp : IdentityProvider
28+ idpBinding : IngressProtocolWebBinding
29+ }
2230
23- type LocalEnrollment =
31+ type LocalEnrollmentContext =
2432 | {
2533 state : 'humanTokenVerified'
2634 captchaTokenPayload : Payload
@@ -30,7 +38,7 @@ type LocalEnrollment =
3038 subject : string
3139 }
3240
33- export type IngressOperations = {
41+ export type IngressUseCases = {
3442 enrollMyself : EnrollMyselfOperation
3543 admitFromIdentityProvider : AdmitFromIdentityProviderOperation
3644}
@@ -40,78 +48,77 @@ export type IngressRoutes = {
4048 idpAdmission : express . Router
4149}
4250
43- function bindingFor ( idpName : string ) : IngressProtocolWebBinding {
44- throw new Error ( 'unimplemented' )
45- }
46-
47- export function CreateIngressRoutes ( ingressApp : IngressOperations , idpRepo : IdentityProviderRepository , tokenService : JWTService , passport : Authenticator ) : IngressRoutes {
51+ export function CreateIngressRoutes ( ingressApp : IngressUseCases , idpRepo : IdentityProviderRepository , tokenService : JWTService , passport : Authenticator ) : IngressRoutes {
4852
49- const captchaBearer = new BearerStrategy ( ( token , done ) => {
50- const expectation = {
51- subject : null ,
52- expiration : null ,
53- assertion : TokenAssertion . IsHuman
53+ const idpBindings = new Map < string , IngressProtocolWebBinding > ( )
54+ async function bindingFor ( idpName : string ) : Promise < IngressProtocolWebBinding | null > {
55+ const idp = await idpRepo . findIdpByName ( idpName )
56+ if ( ! idp ) {
57+ return null
5458 }
55- tokenService . verifyToken ( token , expectation )
56- . then ( payload => done ( null , payload ) )
57- . catch ( err => done ( err ) )
58- } )
59-
60- // TODO: separate routers for /auth/idp/* and /api/users/signups/* for backward compatibility
59+ if ( idp . protocol === 'local' ) {
60+ return LocalBinding ( passport , )
61+ }
62+ throw new Error ( 'unimplemented' )
63+ }
6164
62- const routeToIdp = express . Router ( ) . all ( '/' ,
63- ( ( 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 } ` ) )
69- } ) as express . RequestHandler ,
70- ( async ( err , req , res , next ) => {
71- if ( err ) {
72- console . error ( 'identity provider authentication error:' , err )
73- return res . status ( 500 ) . send ( 'unexpected authentication result' )
74- }
75- if ( req . user ?. from !== 'identityProvider' ) {
76- console . error ( 'unexpected authentication user type:' , req . user ?. from )
77- return res . status ( 500 ) . send ( 'unexpected authentication result' )
78- }
79- const identityProviderName = req . ingress ! . identityProviderService ! . idp . name
80- const identityProviderUser = req . user . account
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 } `
65+ const routeToIdp = express . Router ( )
66+ . all ( '/' ,
67+ ( ( req , res , next ) => {
68+ const idpService = req . ingress ?. idpBinding
69+ if ( ! idpService ) {
70+ return next ( new Error ( `no identity provider for ingress request: ${ req . method } ${ req . originalUrl } ` ) )
9971 }
100- res . redirect ( uri ) ;
101- } else {
102- res . render ( 'authentication' , { host : req . getRoot ( ) , success : true , login : { token : req . token , user : req . user } } ) ;
103- }
104- } ) as express . ErrorRequestHandler
105- )
72+ if ( req . path . endsWith ( '/signin' ) ) {
73+ const userAgentType : UserAgentType = req . params . state === 'mobile' ? UserAgentType . MobileApp : UserAgentType . WebApp
74+ return idpService . beginIngressFlow ( req , res , next , userAgentType )
75+ }
76+ idpService . handleIngressFlowRequest ( req , res , next )
77+ } ) as express . RequestHandler ,
78+ ( async ( err , req , res , next ) => {
79+ if ( err ) {
80+ console . error ( 'identity provider authentication error:' , err )
81+ return res . status ( 500 ) . send ( 'unexpected authentication result' )
82+ }
83+ if ( ! req . user ?. admittingFromIdentityProvider ) {
84+ console . error ( 'unexpected ingress user type:' , req . user )
85+ return res . status ( 500 ) . send ( 'unexpected authentication result' )
86+ }
87+ const idpAdmission = req . user . admittingFromIdentityProvider
88+ const { idpBinding, idp } = req . ingress !
89+ const identityProviderUser = idpAdmission . account
90+ const admission = await ingressApp . admitFromIdentityProvider ( { identityProviderName : idp . name , identityProviderUser } )
91+ if ( admission . error ) {
92+ return next ( admission . error )
93+ }
94+ const { admissionToken, mageAccount } = admission . success
95+ if ( idpBinding . ingressResponseType === IngressResponseType . Direct ) {
96+ return res . json ( { user : mageAccount , token : admissionToken } )
97+ }
98+ if ( idpAdmission . flowState === UserAgentType . MobileApp ) {
99+ if ( mageAccount . active && mageAccount . enabled ) {
100+ return res . redirect ( `mage://app/authentication?token=${ req . token } ` )
101+ }
102+ else {
103+ return res . redirect ( `mage://app/invalid_account?active=${ mageAccount . active } &enabled=${ mageAccount . enabled } ` )
104+ }
105+ }
106+ else if ( idpAdmission . flowState === UserAgentType . WebApp ) {
107+ return res . render ( 'authentication' , { host : req . getRoot ( ) , success : true , login : { token : admissionToken , user : mageAccount } } )
108+ }
109+ return res . status ( 500 ) . send ( 'invalid authentication state' )
110+ } ) as express . ErrorRequestHandler
111+ )
106112
107113 // TODO: mount to /auth
108114 const idpAdmission = express . Router ( )
109115 idpAdmission . use ( '/:identityProviderName' ,
110- ( req , res , next ) => {
116+ async ( req , res , next ) => {
111117 const idpName = req . params . identityProviderName
112- const idpService = bindingFor ( idpName )
113- if ( idpService ) {
114- req . ingress = { state : 'init' , identityProviderService : idpService }
118+ const idp = await idpRepo . findIdpByName ( idpName )
119+ const idpBinding = await bindingFor ( idpName )
120+ if ( idpBinding && idp ) {
121+ req . ingress = { idpBinding, idp }
115122 return next ( )
116123 }
117124 res . status ( 404 ) . send ( `${ idpName } not found` )
@@ -149,6 +156,17 @@ export function CreateIngressRoutes(ingressApp: IngressOperations, idpRepo: Iden
149156 }
150157 } )
151158
159+ const captchaBearer = new BearerStrategy ( ( token , done ) => {
160+ const expectation = {
161+ subject : null ,
162+ expiration : null ,
163+ assertion : TokenAssertion . IsHuman
164+ }
165+ tokenService . verifyToken ( token , expectation )
166+ . then ( payload => done ( null , payload ) )
167+ . catch ( err => done ( err ) )
168+ } )
169+
152170 // TODO: mount to /api/users/signups/verifications
153171 localEnrollment . route ( '/signups/verifications' )
154172 . post (
@@ -163,19 +181,16 @@ export function CreateIngressRoutes(ingressApp: IngressOperations, idpRepo: Iden
163181 if ( ! captchaTokenPayload ) {
164182 return res . status ( 400 ) . send ( 'Missing captcha token' )
165183 }
166- req . ingress = {
167- ...req . ingress ! ,
168- state : 'localEnrollment' ,
169- localEnrollment : { state : 'humanTokenVerified' , captchaTokenPayload } }
184+ req . localEnrollment = { state : 'humanTokenVerified' , captchaTokenPayload }
170185 next ( )
171186 } ) ( req , res , next )
172187 } ,
173188 async ( req , res , next ) => {
174189 try {
175- if ( req . ingress ?. state !== ' localEnrollment' || req . ingress . localEnrollment . state !== 'humanTokenVerified' ) {
190+ if ( req . localEnrollment ? .state !== 'humanTokenVerified' ) {
176191 return res . status ( 500 ) . send ( 'invalid ingress state' )
177192 }
178- const tokenPayload = req . ingress . localEnrollment . captchaTokenPayload
193+ const tokenPayload = req . localEnrollment . captchaTokenPayload
179194 const hashedCaptchaText = tokenPayload . captcha as string
180195 const userCaptchaText = req . body . captchaText
181196 const isHuman = await defaultHashUtil . validPassword ( userCaptchaText , hashedCaptchaText )
0 commit comments