/**
 * Copyright (c) AXA Assistance France
 *
 * Licensed under the AXA Assistance France License (the "License"); you
 * may not use this file except in compliance with the License.
 * A copy of the License can be found in the LICENSE.md file distributed
 * together with this file.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ module import { IncomingMessage } from 'http' ; import express , { RequestHandler , Express } from 'express' ; import cors from 'cors' ; import basicAuth from 'basic-auth' ; import { EventEmitter } from 'events' ; import { v4 as uuidv4 } from 'uuid' ; import { OAuth2Issuer } from './oauth2-issuer' ; import { assertIsString , assertIsValidTokenRequest , defaultTokenTtl , } from './helpers' ; import type { JwtTransform , MutableRedirectUri , MutableResponse , MutableToken , OAuth2Endpoints , OAuth2EndpointsInput , ScopesOrTransform , StatusCodeMutableResponse , } from './types' ; import { Events } from './types' ; import { InternalEvents } from './types-internals' ; import { json } from 'body-parser' ; const DEFAULT_ENDPOINTS = Object . freeze ( { wellKnownDocument : '/.well-known/openid-configuration' , token : '/token' , jwks : '/jwks' , authorize : '/authorize' , userinfo : '/userinfo' , revoke : '/revoke' , endSession : '/endsession' , } ) ; /**
 * Provides a request handler for an OAuth 2 server.
 */ export class OAuth2Service extends EventEmitter { param { OAuth2Issuer } oauth2Issuer param { OAuth2EndpointsInput | undefined } paths #issuer : OAuth2Issuer ; #requestHandler : Express ; #nonce : Record < string , string > ; #endpoints : OAuth2Endpoints ; constructor ( oauth2Issuer , endpoints ? ) { super ( ) ; this . #issuer = oauth2Issuer ; this . #endpoints = { ... DEFAULT_ENDPOINTS , ... endpoints } ; this . #requestHandler = this . buildRequestHandler ( ) ; this . #nonce = { } ; } type { OAuth2Issuer } get issuer ( ) : OAuth2Issuer { return this . #issuer ; } param { IncomingMessage } req param { number } expiresIn param { ScopesOrTransform } scopesOrTransform returns { Promise < string > } fires async buildToken ( req , expiresIn , scopesOrTransform ) { this . issuer . once ( InternalEvents . BeforeSigning , ( token ) => { event param { MutableToken } token param { IncomingMessage } req this . emit ( Events . BeforeTokenSigning , token , req ) ; } ) ; return await this . issuer . buildToken ( { scopesOrTransform , expiresIn } ) ; } type { Function } get requestHandler ( ) : Express { return this . #requestHandler ; } private buildRequestHandler = ( ) => { const app = express ( ) ; app . disable ( 'x-powered-by' ) ; app . use ( json ( ) ) ; app . use ( cors ( ) ) ; app . get ( this . #endpoints . wellKnownDocument , this . openidConfigurationHandler ) ; app . get ( this . #endpoints . jwks , this . jwksHandler ) ; app . post ( this . #endpoints . token , express . urlencoded ( { extended : false } ) , this . tokenHandler ) ; app . get ( this . #endpoints . authorize , this . authorizeHandler ) ; app . get ( this . #endpoints . userinfo , this . userInfoHandler ) ; app . post ( this . #endpoints . revoke , this . revokeHandler ) ; app . get ( this . #endpoints . endSession , this . endSessionHandler ) ; return app ; } ; private openidConfigurationHandler : RequestHandler = ( _req , res ) => { assertIsString ( this . issuer . url , 'Unknown issuer url.' ) ; const openidConfig = { issuer : this . issuer . url , token_endpoint : `${ this . issuer . url }${ this . #endpoints . token }` , authorization_endpoint : `${ this . issuer . url }${ this . #endpoints . authorize }` , userinfo_endpoint : `${ this . issuer . url }${ this . #endpoints . userinfo }` , token_endpoint_auth_methods_supported : [ 'none' ] , jwks_uri : `${ this . issuer . url }${ this . #endpoints . jwks }` , response_types_supported : [ 'code' ] , grant_types_supported : [ 'client_credentials' , 'authorization_code' , 'password' , ] , token_endpoint_auth_signing_alg_values_supported : [ 'RS256' ] , response_modes_supported : [ 'query' ] , id_token_signing_alg_values_supported : [ 'RS256' ] , revocation_endpoint : `${ this . issuer . url }${ this . #endpoints . revoke }` , subject_types_supported : [ 'public' ] , end_session_endpoint : `${ this . issuer . url }${ this . #endpoints . endSession }` , } ; return res . json ( openidConfig ) ; } ; private jwksHandler : RequestHandler = ( _req , res ) => { res . json ( { keys : this . issuer . keys . toJSON ( ) } ) ; } ; private tokenHandler : RequestHandler = async ( req , res , next ) => { try { const tokenTtl = defaultTokenTtl ; res . set ( { 'Cache-Control' : 'no-store' , Pragma : 'no-cache' , } ) ; let xfn ; assertIsValidTokenRequest ( req . body ) ; const reqBody = req . body ; let { scope } = reqBody ; switch ( req . body . grant_type ) { case 'client_credentials' : xfn = scope ; break ; case 'password' : xfn = ( _header , payload ) => { Object . assign ( payload , { sub : reqBody . username , amr : [ 'pwd' ] , scope , } ) ; } ; break ; case 'authorization_code' : scope = 'dummy' ; xfn = ( _header , payload ) => { Object . assign ( payload , { sub : 'johndoe' , amr : [ 'pwd' ] , scope , } ) ; } ; break ; case 'refresh_token' : scope = 'dummy' ; xfn = ( _header , payload ) => { Object . assign ( payload , { sub : 'johndoe' , amr : [ 'pwd' ] , scope , } ) ; } ; break ; default : return res . status ( 400 ) . json ( { error : 'invalid_grant' , } ) ; } const token = await this . buildToken ( req , tokenTtl , xfn ) ; const body = { access_token : token , token_type : 'Bearer' , expires_in : tokenTtl , scope , } ; if ( req . body . grant_type !== 'client_credentials' ) { const credentials = basicAuth ( req ) ; const clientId = credentials ? credentials . name : req . body . client_id ; const xfn = ( _header , payload ) => { Object . assign ( payload , { sub : 'johndoe' , aud : clientId , } ) ; if ( reqBody . code !== undefined && this . #nonce [ reqBody . code ] ) { Object . assign ( payload , { nonce : this . #nonce [ reqBody . code ] , } ) ; delete this . #nonce [ reqBody . code ] ; } } ; body . id_token = await this . buildToken ( req , tokenTtl , xfn ) ; body . refresh_token = uuidv4 ( ) ; } const tokenEndpointResponse = { body , statusCode : 200 , } ; event param { MutableResponse } response param { IncomingMessage } req this . emit ( Events . BeforeResponse , tokenEndpointResponse , req ) ; return res . status ( tokenEndpointResponse . statusCode ) . json ( tokenEndpointResponse . body ) ; } catch ( e ) { return next ( e ) ; } } ; private authorizeHandler : RequestHandler = ( req , res ) => { const { scope , state } = req . query ; const responseType = req . query . response_type ; const redirectUri = req . query . redirect_uri ; const code = uuidv4 ( ) ; let queryNonce ; if ( 'nonce' in req . query ) { assertIsString ( req . query . nonce , 'Invalid nonce type' ) ; queryNonce = req . query . nonce ; } assertIsString ( redirectUri , 'Invalid redirectUri type' ) ; assertIsString ( scope , 'Invalid scope type' ) ; assertIsString ( state , 'Invalid state type' ) ; const url = new URL ( redirectUri ) ; if ( responseType === 'code' ) { if ( queryNonce !== undefined ) { this . #nonce [ code ] = queryNonce ; } url . searchParams . set ( 'code' , code ) ; url . searchParams . set ( 'scope' , scope ) ; url . searchParams . set ( 'state' , state ) ; } else { url . searchParams . set ( 'error' , 'unsupported_response_type' ) ; url . searchParams . set ( 'error_description' , 'The authorization server does not support obtaining an access token using this response_type.' ) ; url . searchParams . set ( 'state' , state ) ; } const authorizeRedirectUri = { url } ; event param { MutableRedirectUri } authorizeRedirectUri param { IncomingMessage } req this . emit ( Events . BeforeAuthorizeRedirect , authorizeRedirectUri , req ) ; res . redirect ( url . href ) ; } ; private userInfoHandler : RequestHandler = ( req , res ) => { const userInfoResponse = { body : { sub : 'johndoe' , } , statusCode : 200 , } ; event param { MutableResponse } response param { IncomingMessage } req this . emit ( Events . BeforeUserinfo , userInfoResponse , req ) ; res . status ( userInfoResponse . statusCode ) . json ( userInfoResponse . body ) ; } ; private revokeHandler : RequestHandler = ( req , res ) => { const revokeResponse = { statusCode : 200 , } ; event param { StatusCodeMutableResponse } response param { IncomingMessage } req this . emit ( Events . BeforeRevoke , revokeResponse , req ) ; return res . status ( revokeResponse . statusCode ) . send ( '' ) ; } ; private endSessionHandler : RequestHandler = ( req , res ) => { assertIsString ( req . query . post_logout_redirect_uri , 'Invalid post_logout_redirect_uri type' ) ; const postLogoutRedirectUri = { url : new URL ( req . query . post_logout_redirect_uri ) , } ; event param { MutableRedirectUri } postLogoutRedirectUri param { IncomingMessage } req this . emit ( Events . BeforePostLogoutRedirect , postLogoutRedirectUri , req ) ; res . redirect ( postLogoutRedirectUri . url . href ) ; } ; }