2023-03-08 16:41:59 +01:00
'use strict' ;
exports = module . exports = {
2023-03-21 13:54:40 +01:00
start ,
stop ,
2023-03-24 20:08:17 +01:00
revokeByUserId ,
2023-03-16 15:37:03 +01:00
clients : {
add : clientsAdd ,
get : clientsGet ,
del : clientsDel ,
update : clientsUpdate ,
list : clientsList
2023-03-14 14:19:29 +01:00
}
2023-03-08 16:41:59 +01:00
} ;
2023-03-10 17:13:33 +01:00
const assert = require ( 'assert' ) ,
2023-04-14 21:18:44 +02:00
apps = require ( './apps.js' ) ,
2023-08-26 08:18:58 +05:30
AuditSource = require ( './auditsource.js' ) ,
2023-03-16 15:37:03 +01:00
BoxError = require ( './boxerror.js' ) ,
2023-03-23 18:02:45 +01:00
blobs = require ( './blobs.js' ) ,
2023-08-02 21:01:11 +05:30
branding = require ( './branding.js' ) ,
2023-03-21 14:46:09 +01:00
constants = require ( './constants.js' ) ,
2024-01-23 12:44:23 +01:00
crypto = require ( 'crypto' ) ,
2023-08-11 19:41:05 +05:30
dashboard = require ( './dashboard.js' ) ,
2023-03-16 15:37:03 +01:00
database = require ( './database.js' ) ,
2023-03-10 17:13:33 +01:00
debug = require ( 'debug' ) ( 'box:oidc' ) ,
2023-08-14 11:08:38 +05:30
dns = require ( './dns.js' ) ,
2023-03-17 14:45:45 +01:00
ejs = require ( 'ejs' ) ,
2023-03-21 14:46:09 +01:00
express = require ( 'express' ) ,
2023-07-24 19:24:03 +02:00
eventlog = require ( './eventlog.js' ) ,
2023-03-08 16:41:59 +01:00
fs = require ( 'fs' ) ,
2023-08-11 13:36:46 +02:00
marked = require ( 'marked' ) ,
2023-03-14 10:47:01 +01:00
middleware = require ( './middleware' ) ,
2023-03-08 16:41:59 +01:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
2023-03-21 14:46:09 +01:00
http = require ( 'http' ) ,
2023-03-13 17:01:52 +01:00
HttpError = require ( 'connect-lastmile' ) . HttpError ,
2023-03-23 18:02:45 +01:00
jose = require ( 'jose' ) ,
2023-03-13 17:01:52 +01:00
safe = require ( 'safetydance' ) ,
2023-03-21 14:46:09 +01:00
settings = require ( './settings.js' ) ,
2023-06-02 20:47:36 +02:00
tokens = require ( './tokens.js' ) ,
2023-06-19 11:50:53 +02:00
translation = require ( './translation.js' ) ,
2023-04-14 21:18:44 +02:00
url = require ( 'url' ) ,
2023-03-21 14:46:09 +01:00
users = require ( './users.js' ) ,
util = require ( 'util' ) ;
2023-03-08 16:41:59 +01:00
2023-03-16 15:37:03 +01:00
const OIDC _CLIENTS _TABLE _NAME = 'oidcClients' ;
2023-07-20 13:26:07 +02:00
const OIDC _CLIENTS _FIELDS = [ 'id' , 'secret' , 'name' , 'appId' , 'loginRedirectUri' , 'tokenSignatureAlgorithm' ] ;
2023-03-16 15:37:03 +01:00
2023-03-21 14:39:58 +01:00
const ROUTE _PREFIX = '/openid' ;
2023-04-04 15:38:45 +02:00
const DEFAULT _TOKEN _SIGNATURE _ALGORITHM = 'RS256' ;
2023-03-21 14:39:58 +01:00
let gHttpServer = null ;
2023-03-23 09:27:40 +01:00
// -----------------------------
// Database model
// -----------------------------
2023-04-04 15:38:45 +02:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
result . tokenSignatureAlgorithm = result . tokenSignatureAlgorithm || DEFAULT _TOKEN _SIGNATURE _ALGORITHM ;
return result ;
}
2023-03-23 09:27:40 +01:00
async function clientsAdd ( id , data ) {
2023-03-16 15:37:03 +01:00
assert . strictEqual ( typeof id , 'string' ) ;
2023-03-23 09:27:40 +01:00
assert . strictEqual ( typeof data . secret , 'string' ) ;
assert . strictEqual ( typeof data . loginRedirectUri , 'string' ) ;
assert . strictEqual ( typeof data . name , 'string' ) ;
assert . strictEqual ( typeof data . appId , 'string' ) ;
2023-04-04 15:38:45 +02:00
assert ( data . tokenSignatureAlgorithm === 'RS256' || data . tokenSignatureAlgorithm === 'EdDSA' ) ;
2023-03-16 15:37:03 +01:00
2023-07-20 13:26:07 +02:00
const query = ` INSERT INTO ${ OIDC _CLIENTS _TABLE _NAME } (id, secret, name, appId, loginRedirectUri, tokenSignatureAlgorithm) VALUES (?, ?, ?, ?, ?, ?) ` ;
const args = [ id , data . secret , data . name , data . appId , data . loginRedirectUri , data . tokenSignatureAlgorithm ] ;
2023-03-16 15:37:03 +01:00
const [ error ] = await safe ( database . query ( query , args ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'client already exists' ) ;
if ( error ) throw error ;
}
async function clientsGet ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2023-08-07 16:53:00 +02:00
if ( id === tokens . ID _WEBADMIN ) {
2023-08-11 19:41:05 +05:30
const { fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
2023-06-02 20:47:36 +02:00
return {
2023-08-07 16:53:00 +02:00
id : tokens . ID _WEBADMIN ,
2023-06-02 20:47:36 +02:00
secret : 'notused' ,
2023-06-04 16:29:55 +02:00
application _type : 'web' ,
2023-06-02 20:47:36 +02:00
response _types : [ 'code' , 'code token' ] ,
grant _types : [ 'authorization_code' , 'implicit' ] ,
2023-08-11 19:41:05 +05:30
loginRedirectUri : ` https:// ${ dashboardFqdn } /authcallback.html `
2023-06-02 20:47:36 +02:00
} ;
2023-08-07 16:53:00 +02:00
} else if ( id === tokens . ID _DEVELOPMENT ) {
2023-06-15 15:08:09 +02:00
return {
2023-08-07 16:53:00 +02:00
id : tokens . ID _DEVELOPMENT ,
2023-06-15 15:08:09 +02:00
secret : 'notused' ,
application _type : 'native' , // have to use native here to support plaintext http, this however makes it impossible to skip consent screen
response _types : [ 'code' , 'code token' ] ,
grant _types : [ 'authorization_code' , 'implicit' ] ,
loginRedirectUri : 'http://localhost:4000/authcallback.html'
} ;
2023-06-02 20:47:36 +02:00
}
2023-03-16 15:37:03 +01:00
const result = await database . query ( ` SELECT ${ OIDC _CLIENTS _FIELDS } FROM ${ OIDC _CLIENTS _TABLE _NAME } WHERE id = ? ` , [ id ] ) ;
if ( result . length === 0 ) return null ;
2023-04-04 15:38:45 +02:00
return postProcess ( result [ 0 ] ) ;
2023-03-16 15:37:03 +01:00
}
2023-03-23 09:27:40 +01:00
async function clientsUpdate ( id , data ) {
2023-03-16 15:37:03 +01:00
assert . strictEqual ( typeof id , 'string' ) ;
2023-03-23 09:27:40 +01:00
assert . strictEqual ( typeof data . loginRedirectUri , 'string' ) ;
assert . strictEqual ( typeof data . name , 'string' ) ;
assert . strictEqual ( typeof data . appId , 'string' ) ;
2023-04-04 15:38:45 +02:00
assert ( data . tokenSignatureAlgorithm === 'RS256' || data . tokenSignatureAlgorithm === 'EdDSA' ) ;
2023-03-16 15:37:03 +01:00
2023-10-06 14:11:54 +02:00
const result = await database . query ( ` UPDATE ${ OIDC _CLIENTS _TABLE _NAME } SET name=?, appId=?, loginRedirectUri=?, tokenSignatureAlgorithm=? WHERE id = ? ` , [ data . name , data . appId , data . loginRedirectUri , data . tokenSignatureAlgorithm , id ] ) ;
2023-03-16 15:37:03 +01:00
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'client not found' ) ;
}
async function clientsDel ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
const result = await database . query ( ` DELETE FROM ${ OIDC _CLIENTS _TABLE _NAME } WHERE id = ? ` , [ id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'client not found' ) ;
}
async function clientsList ( ) {
2023-12-03 18:06:36 +01:00
const results = await database . query ( ` SELECT * FROM ${ OIDC _CLIENTS _TABLE _NAME } ORDER BY name ASC ` , [ ] ) ;
2023-04-04 15:38:45 +02:00
results . forEach ( postProcess ) ;
2023-03-16 15:37:03 +01:00
return results ;
}
2023-03-14 14:58:09 +01:00
2023-03-24 20:08:17 +01:00
// -----------------------------
// Basic in-memory json file backed based data store
// -----------------------------
const DATA _STORE = { } ;
function load ( modelName ) {
assert . strictEqual ( typeof modelName , 'string' ) ;
if ( DATA _STORE [ modelName ] ) return ;
const filePath = path . join ( paths . OIDC _STORE _DIR , ` ${ modelName } .json ` ) ;
2023-07-25 12:22:54 +05:30
// debug(`load: model ${modelName} based on ${filePath}.`);
2023-03-24 20:08:17 +01:00
let data = { } ;
try {
data = JSON . parse ( fs . readFileSync ( filePath ) , 'utf8' ) ;
} catch ( e ) {
2023-06-04 16:03:45 +02:00
if ( e . code === 'ENOENT' ) debug ( ` load: failed to read ${ filePath } , start with new one. ` ) ;
else debug ( ` load: failed to read ${ filePath } , use in-memory. %o ` , e ) ;
2023-03-24 20:08:17 +01:00
}
DATA _STORE [ modelName ] = data ;
}
function save ( modelName ) {
assert . strictEqual ( typeof modelName , 'string' ) ;
if ( ! DATA _STORE [ modelName ] ) return ;
const filePath = path . join ( paths . OIDC _STORE _DIR , ` ${ modelName } .json ` ) ;
2023-07-25 12:22:54 +05:30
// debug(`save: model ${modelName} to ${filePath}.`);
2023-03-24 20:08:17 +01:00
try {
2023-06-04 16:29:55 +02:00
fs . writeFileSync ( filePath , JSON . stringify ( DATA _STORE [ modelName ] , null , 2 ) , 'utf8' ) ;
2023-03-24 20:08:17 +01:00
} catch ( e ) {
2023-05-11 16:22:58 +02:00
debug ( ` save: failed to write ${ filePath } ` , e ) ;
2023-03-24 20:08:17 +01:00
}
}
// -----------------------------
// Session, Grant and Token management
// -----------------------------
// This is based on the same storage as the below CloudronAdapter
async function revokeByUserId ( userId ) {
assert . strictEqual ( typeof userId , 'string' ) ;
function revokeObjects ( modelName ) {
load ( modelName ) ;
for ( let id in DATA _STORE [ modelName ] ) {
if ( DATA _STORE [ modelName ] [ id ] . payload ? . accountId === userId ) delete DATA _STORE [ modelName ] [ id ] ;
}
save ( modelName ) ;
}
revokeObjects ( 'Session' ) ;
revokeObjects ( 'Grant' ) ;
revokeObjects ( 'AuthorizationCode' ) ;
2023-06-04 16:03:45 +02:00
revokeObjects ( 'AccessToken' ) ;
2023-03-24 20:08:17 +01:00
}
2023-03-23 09:27:40 +01:00
// -----------------------------
// Generic oidc node module data store model
// -----------------------------
2023-03-08 16:41:59 +01:00
class CloudronAdapter {
2023-03-21 14:46:09 +01:00
/ * *
*
* Creates an instance of MyAdapter for an oidc - provider model .
*
* @ constructor
* @ param { string } name Name of the oidc - provider model . One of "Grant, " Session ", " AccessToken " ,
* "AuthorizationCode" , "RefreshToken" , "ClientCredentials" , "Client" , "InitialAccessToken" ,
* "RegistrationAccessToken" , "DeviceCode" , "Interaction" , "ReplayDetection" ,
* "BackchannelAuthenticationRequest" , or "PushedAuthorizationRequest"
*
* /
2023-03-08 16:41:59 +01:00
constructor ( name ) {
this . name = name ;
2023-07-25 12:22:54 +05:30
// debug(`Creating OpenID storage adapter for ${name}`);
2023-03-14 14:58:09 +01:00
2023-06-04 16:03:45 +02:00
if ( this . name === 'Client' ) {
2023-06-02 20:47:36 +02:00
return ;
} else {
2023-03-24 20:08:17 +01:00
load ( name ) ;
2023-03-14 14:58:09 +01:00
}
2023-03-08 16:41:59 +01:00
}
2023-03-21 14:46:09 +01:00
/ * *
*
* Update or Create an instance of an oidc - provider model .
*
* @ return { Promise } Promise fulfilled when the operation succeeded . Rejected with error when
* encountered .
* @ param { string } id Identifier that oidc - provider will use to reference this model instance for
* future operations .
* @ param { object } payload Object with all properties intended for storage .
* @ param { integer } expiresIn Number of seconds intended for this model to be stored .
*
* /
2023-03-08 16:41:59 +01:00
async upsert ( id , payload , expiresIn ) {
2023-03-16 15:37:03 +01:00
if ( this . name === 'Client' ) {
2023-04-30 10:18:39 +02:00
debug ( 'upsert: this should not happen as it is stored in our db' ) ;
2023-08-07 16:53:00 +02:00
} else if ( this . name === 'AccessToken' && ( payload . clientId === tokens . ID _WEBADMIN || payload . clientId === tokens . ID _DEVELOPMENT ) ) {
2023-06-02 20:47:36 +02:00
const clientId = payload . clientId ;
const identifier = payload . accountId ;
const expires = Date . now ( ) + constants . DEFAULT _TOKEN _EXPIRATION _MSECS ;
const accessToken = id ;
const [ error ] = await safe ( tokens . add ( { clientId , identifier , expires , accessToken } ) ) ;
if ( error ) {
console . log ( 'Error adding access token' , error ) ;
throw error ;
}
2023-03-16 15:37:03 +01:00
} else {
2023-03-24 20:08:17 +01:00
DATA _STORE [ this . name ] [ id ] = { id , expiresIn , payload , consumed : false } ;
save ( this . name ) ;
2023-03-16 15:37:03 +01:00
}
2023-03-08 16:41:59 +01:00
}
2023-03-21 14:46:09 +01:00
/ * *
*
* Return previously stored instance of an oidc - provider model .
*
* @ return { Promise } Promise fulfilled with what was previously stored for the id ( when found and
* not dropped yet due to expiration ) or falsy value when not found anymore . Rejected with error
* when encountered .
* @ param { string } id Identifier of oidc - provider model
*
* /
2023-03-08 16:41:59 +01:00
async find ( id ) {
2023-03-16 15:37:03 +01:00
if ( this . name === 'Client' ) {
const [ error , client ] = await safe ( clientsGet ( id ) ) ;
2023-03-21 15:23:45 +01:00
if ( error ) {
2023-04-30 10:18:39 +02:00
debug ( 'find: error getting client' , error ) ;
2023-03-21 15:23:45 +01:00
return null ;
}
if ( ! client ) return null ;
2023-03-16 15:37:03 +01:00
2023-04-14 21:18:44 +02:00
const tmp = { } ;
2023-06-20 19:58:09 +02:00
tmp . application _type = client . application _type || 'native' ; // default is web but we want more flexible redirectUris and this is only used in https://github.com/panva/node-oidc-provider/blob/03c9bc513860e68ee7be84f99bfc9dc930b224e8/lib/helpers/client_schema.js#L536
2023-04-14 21:18:44 +02:00
tmp . client _id = id ;
tmp . client _secret = client . secret ;
tmp . id _token _signed _response _alg = client . tokenSignatureAlgorithm || 'RS256' ;
2023-03-22 11:12:50 +01:00
2023-06-02 20:47:36 +02:00
if ( client . response _types ) tmp . response _types = client . response _types ;
if ( client . grant _types ) tmp . grant _types = client . grant _types ;
2023-04-14 21:18:44 +02:00
if ( client . appId ) {
const [ error , app ] = await safe ( apps . get ( client . appId ) ) ;
if ( error || ! app ) {
2023-04-30 10:18:39 +02:00
debug ( ` find: Unknown app for client with appId ${ client . appId } ` ) ;
2023-04-14 21:18:44 +02:00
return null ;
}
2023-07-20 13:26:07 +02:00
// prefix login redirect uris with app.fqdn if it is just a path without a schema
2023-04-14 21:18:44 +02:00
// native callbacks for apps have custom schema like app.immich:/
tmp . redirect _uris = client . loginRedirectUri . split ( ',' ) . map ( s => s . trim ( ) ) . map ( s => url . parse ( s ) . protocol ? s : ` https:// ${ app . fqdn } ${ s } ` ) ;
} else {
tmp . redirect _uris = client . loginRedirectUri . split ( ',' ) . map ( s => s . trim ( ) ) ;
}
2023-03-22 11:12:50 +01:00
2023-06-02 20:47:36 +02:00
return tmp ;
} else if ( this . name === 'AccessToken' ) {
debug ( 'find: we dont support finding AccessTokens' , id ) ;
const [ error , result ] = await safe ( tokens . getByAccessToken ( id ) ) ;
if ( error || ! result ) {
2023-06-04 16:03:45 +02:00
debug ( ` find: Unknown accessToken for id ${ id } maybe oidc internal? ` ) ;
if ( ! DATA _STORE [ this . name ] [ id ] ) return null ;
return DATA _STORE [ this . name ] [ id ] . payload ;
2023-06-02 20:47:36 +02:00
}
const tmp = {
accountId : result . identifier ,
clientId : result . clientId
} ;
2023-03-22 11:12:50 +01:00
return tmp ;
2023-03-16 15:37:03 +01:00
} else {
2023-03-24 20:08:17 +01:00
if ( ! DATA _STORE [ this . name ] [ id ] ) return null ;
2023-03-08 16:41:59 +01:00
2023-03-24 20:08:17 +01:00
return DATA _STORE [ this . name ] [ id ] . payload ;
2023-03-16 15:37:03 +01:00
}
2023-03-08 16:41:59 +01:00
}
2023-03-21 14:46:09 +01:00
/ * *
*
* Return previously stored instance of DeviceCode by the end - user entered user code . You only
* need this method for the deviceFlow feature
*
* @ return { Promise } Promise fulfilled with the stored device code object ( when found and not
* dropped yet due to expiration ) or falsy value when not found anymore . Rejected with error
* when encountered .
* @ param { string } userCode the user _code value associated with a DeviceCode instance
*
* /
2023-03-08 16:41:59 +01:00
async findByUserCode ( userCode ) {
debug ( ` [ ${ this . name } ] FIXME findByUserCode userCode: ${ userCode } ` ) ;
}
2023-03-21 14:46:09 +01:00
/ * *
*
* Return previously stored instance of Session by its uid reference property .
*
* @ return { Promise } Promise fulfilled with the stored session object ( when found and not
* dropped yet due to expiration ) or falsy value when not found anymore . Rejected with error
* when encountered .
* @ param { string } uid the uid value associated with a Session instance
*
* /
2023-03-08 16:41:59 +01:00
async findByUid ( uid ) {
2023-06-02 20:47:36 +02:00
if ( this . name === 'Client' || this . name === 'AccessToken' ) {
2023-04-30 10:18:39 +02:00
debug ( 'findByUid: this should not happen as it is stored in our db' ) ;
2023-03-16 15:37:03 +01:00
} else {
2023-03-24 20:08:17 +01:00
for ( let d in DATA _STORE [ this . name ] ) {
if ( DATA _STORE [ this . name ] [ d ] . payload . uid === uid ) return DATA _STORE [ this . name ] [ d ] . payload ;
2023-03-16 15:37:03 +01:00
}
2023-03-08 16:41:59 +01:00
2023-03-16 15:37:03 +01:00
return false ;
}
2023-03-08 16:41:59 +01:00
}
2023-03-21 14:46:09 +01:00
/ * *
*
* Mark a stored oidc - provider model as consumed ( not yet expired though ! ) . Future finds for this
* id should be fulfilled with an object containing additional property named "consumed" with a
* truthy value ( timestamp , date , boolean , etc ) .
*
* @ return { Promise } Promise fulfilled when the operation succeeded . Rejected with error when
* encountered .
* @ param { string } id Identifier of oidc - provider model
*
* /
2023-03-08 16:41:59 +01:00
async consume ( id ) {
2023-06-04 16:03:45 +02:00
debug ( ` [ ${ this . name } ] consume: ${ id } ` ) ;
if ( this . name === 'Client' ) {
2023-04-30 10:18:39 +02:00
debug ( 'consume: this should not happen as it is stored in our db' ) ;
2023-03-16 15:37:03 +01:00
} else {
2023-03-24 20:08:17 +01:00
if ( DATA _STORE [ this . name ] [ id ] ) DATA _STORE [ this . name ] [ id ] . consumed = true ;
save ( this . name ) ;
2023-03-16 15:37:03 +01:00
}
2023-03-08 16:41:59 +01:00
}
2023-03-21 14:46:09 +01:00
/ * *
*
* Destroy / Drop / Remove a stored oidc - provider model . Future finds for this id should be fulfilled
* with falsy values .
*
* @ return { Promise } Promise fulfilled when the operation succeeded . Rejected with error when
* encountered .
* @ param { string } id Identifier of oidc - provider model
*
* /
2023-03-08 16:41:59 +01:00
async destroy ( id ) {
2023-07-25 12:22:54 +05:30
// debug(`[${this.name}] destroy: ${id}`);
2023-06-04 16:03:45 +02:00
if ( this . name === 'Client' ) {
2023-04-30 10:18:39 +02:00
debug ( 'destroy: this should not happen as it is stored in our db' ) ;
2023-03-16 15:37:03 +01:00
} else {
2023-03-24 20:08:17 +01:00
delete DATA _STORE [ this . name ] [ id ] ;
save ( this . name ) ;
2023-03-16 15:37:03 +01:00
}
2023-03-08 16:41:59 +01:00
}
2023-03-21 14:46:09 +01:00
/ * *
*
* Destroy / Drop / Remove a stored oidc - provider model by its grantId property reference . Future
* finds for all tokens having this grantId value should be fulfilled with falsy values .
*
* @ return { Promise } Promise fulfilled when the operation succeeded . Rejected with error when
* encountered .
* @ param { string } grantId the grantId value associated with a this model ' s instance
*
* /
2023-03-08 16:41:59 +01:00
async revokeByGrantId ( grantId ) {
2023-06-04 16:03:45 +02:00
debug ( ` [ ${ this . name } ] revokeByGrantId: ${ grantId } ` ) ;
if ( this . name === 'Client' ) {
2023-04-30 10:18:39 +02:00
debug ( 'revokeByGrantId: this should not happen as it is stored in our db' ) ;
2023-03-16 15:37:03 +01:00
} else {
2023-03-24 20:08:17 +01:00
for ( let d in DATA _STORE [ this . name ] ) {
if ( DATA _STORE [ this . name ] [ d ] . grantId === grantId ) {
delete DATA _STORE [ this . name ] [ d ] ;
return save ( this . name ) ;
2023-03-16 15:37:03 +01:00
}
2023-03-08 16:41:59 +01:00
}
}
}
}
2023-03-23 09:27:40 +01:00
// -----------------------------
// Route handler
// -----------------------------
2023-03-21 14:39:58 +01:00
function renderInteractionPage ( provider ) {
2023-03-10 17:13:33 +01:00
assert . strictEqual ( typeof provider , 'object' ) ;
2023-07-04 16:23:59 +02:00
return async function ( req , res ) {
2023-06-19 11:50:53 +02:00
const translationAssets = await translation . getTranslations ( ) ;
2023-03-10 17:13:33 +01:00
try {
const { uid , prompt , params , session } = await provider . interactionDetails ( req , res ) ;
2023-04-25 13:13:04 +02:00
const client = await clientsGet ( params . client _id ) ;
2023-03-10 17:13:33 +01:00
2023-05-12 13:32:43 +02:00
let app = null ;
if ( client . appId ) app = await apps . get ( client . appId ) ;
2023-03-10 17:13:33 +01:00
switch ( prompt . name ) {
2023-03-21 14:46:09 +01:00
case 'login' : {
2023-05-12 13:32:43 +02:00
const options = {
2023-03-21 14:46:09 +01:00
submitUrl : ` ${ ROUTE _PREFIX } /interaction/ ${ uid } /login ` ,
2023-05-12 13:32:43 +02:00
iconUrl : '/api/v1/cloudron/avatar' ,
2023-08-11 13:36:46 +02:00
name : client ? . name || await branding . getCloudronName ( ) ,
2023-08-17 13:33:31 +02:00
footer : marked . parse ( await branding . renderFooter ( ) ) ,
2023-08-17 13:45:07 +02:00
note : ( client . id === tokens . ID _WEBADMIN && constants . DEMO ) ? '<div style="text-align: center;">This is a demo. Username and password is "cloudron"</div>' : ''
2023-05-12 13:32:43 +02:00
} ;
if ( app ) {
options . name = app . label || app . fqdn ;
2023-05-12 14:31:26 +02:00
options . iconUrl = app . iconUrl ;
2023-05-12 13:32:43 +02:00
}
2023-06-19 11:50:53 +02:00
const template = fs . readFileSync ( _ _dirname + '/oidc_templates/login.ejs' , 'utf-8' ) ;
const html = ejs . render ( translation . translate ( template , translationAssets . translations || { } , translationAssets . fallback || { } ) , options ) ;
return res . send ( html ) ;
2023-03-21 14:46:09 +01:00
}
case 'consent' : {
2023-04-25 13:13:04 +02:00
const options = {
hasAccess : false ,
submitUrl : '' ,
2023-05-12 13:32:43 +02:00
iconUrl : '/api/v1/cloudron/avatar' ,
2023-08-11 13:36:46 +02:00
name : client ? . name || '' ,
footer : marked . parse ( await branding . renderFooter ( ) )
2023-04-25 13:13:04 +02:00
} ;
// check if user has access to the app if client refers to an app
2023-05-12 13:32:43 +02:00
if ( app ) {
2023-04-25 13:13:04 +02:00
const user = await users . get ( session . accountId ) ;
options . name = app . label || app . fqdn ;
2023-05-12 14:31:26 +02:00
options . iconUrl = app . iconUrl ;
2023-04-25 13:13:04 +02:00
options . hasAccess = apps . canAccess ( app , user ) ;
} else {
options . hasAccess = true ;
}
options . submitUrl = ` ${ ROUTE _PREFIX } /interaction/ ${ uid } / ${ options . hasAccess ? 'confirm' : 'abort' } ` ;
return res . render ( 'interaction' , options ) ;
2023-03-21 14:46:09 +01:00
}
default :
return undefined ;
2023-03-10 17:13:33 +01:00
}
2023-03-14 10:47:01 +01:00
} catch ( error ) {
2023-04-30 10:18:39 +02:00
debug ( 'route interaction get error' , error ) ;
2023-08-11 18:38:03 +05:30
return res . render ( 'error' , {
errorMessage : error . error _description || 'Internal error' ,
footer : marked . parse ( await branding . renderFooter ( ) )
} ) ;
2023-03-10 17:13:33 +01:00
}
2023-03-14 14:19:29 +01:00
} ;
}
function interactionLogin ( provider ) {
assert . strictEqual ( typeof provider , 'object' ) ;
2023-03-10 17:13:33 +01:00
2023-03-14 14:19:29 +01:00
return async function ( req , res , next ) {
2023-03-13 19:08:41 +01:00
const [ detailsError , details ] = await safe ( provider . interactionDetails ( req , res ) ) ;
2024-04-04 17:32:58 +02:00
if ( detailsError ) {
if ( detailsError . error _description === 'interaction session not found' ) return next ( new HttpError ( 410 , 'session timeout' ) ) ;
return next ( new HttpError ( 400 , detailsError ) ) ;
}
2023-03-10 17:13:33 +01:00
2023-07-24 19:24:03 +02:00
const ip = req . headers [ 'x-forwarded-for' ] || req . connection . remoteAddress || null ;
const userAgent = req . headers [ 'user-agent' ] || '' ;
const clientId = details . params . client _id ;
2023-03-10 17:13:33 +01:00
2023-07-24 19:24:03 +02:00
debug ( ` interactionLogin: for OpenID client ${ clientId } from ${ ip } ` ) ;
2023-03-13 17:01:52 +01:00
2024-04-03 18:11:21 +02:00
// This is the auto login via token hack
2024-04-03 18:23:29 +02:00
if ( req . body . autoLoginToken ) {
if ( typeof req . body . autoLoginToken !== 'string' ) return next ( new HttpError ( 400 , 'autoLoginToken must be string if provided' ) ) ;
2024-04-03 18:11:21 +02:00
2024-04-03 18:23:29 +02:00
const token = await tokens . getByAccessToken ( req . body . autoLoginToken ) ;
2024-04-03 18:11:21 +02:00
if ( ! token ) return next ( new HttpError ( 401 , 'No such token' ) ) ;
const user = await users . get ( token . identifier ) ;
if ( ! user ) return next ( new HttpError ( 401 , 'User not found' ) ) ;
if ( ! user . active ) return next ( new HttpError ( 401 , 'User not active' ) ) ;
const result = {
login : {
accountId : user . id ,
} ,
} ;
const [ interactionFinishError , redirectTo ] = await safe ( provider . interactionResult ( req , res , result ) ) ;
if ( interactionFinishError ) return next ( new HttpError ( 500 , interactionFinishError ) ) ;
const auditSource = AuditSource . fromOidcRequest ( req ) ;
await eventlog . add ( user . ghost ? eventlog . ACTION _USER _LOGIN _GHOST : eventlog . ACTION _USER _LOGIN , auditSource , { userId : user . id , user : users . removePrivateFields ( user ) , appId : clientId } ) ;
if ( ! user . ghost ) safe ( users . notifyLoginLocation ( user , ip , userAgent , auditSource ) , { debug } ) ;
2024-04-04 10:29:36 +02:00
// clear token as it is one-time use
await tokens . delByAccessToken ( req . body . autoLoginToken ) ;
2024-04-03 18:11:21 +02:00
return res . status ( 200 ) . send ( { redirectTo } ) ;
}
2023-03-13 19:08:41 +01:00
if ( ! req . body . username || typeof req . body . username !== 'string' ) return next ( new HttpError ( 400 , 'A username must be non-empty string' ) ) ;
if ( ! req . body . password || typeof req . body . password !== 'string' ) return next ( new HttpError ( 400 , 'A password must be non-empty string' ) ) ;
if ( 'totpToken' in req . body && typeof req . body . totpToken !== 'string' ) return next ( new HttpError ( 400 , 'totpToken must be a string' ) ) ;
2023-03-13 17:01:52 +01:00
2023-03-13 19:08:41 +01:00
const { username , password , totpToken } = req . body ;
2023-03-13 17:01:52 +01:00
2023-03-13 19:08:41 +01:00
const verifyFunc = username . indexOf ( '@' ) === - 1 ? users . verifyWithUsername : users . verifyWithEmail ;
2023-03-13 17:01:52 +01:00
2024-04-03 18:11:21 +02:00
const [ verifyError , user ] = await safe ( verifyFunc ( username , password , users . AP _WEBADMIN , { totpToken , skipTotpCheck : false } ) ) ;
2023-03-13 19:08:41 +01:00
if ( verifyError && verifyError . reason === BoxError . INVALID _CREDENTIALS ) return next ( new HttpError ( 401 , verifyError . message ) ) ;
2023-05-12 14:31:26 +02:00
if ( verifyError && verifyError . reason === BoxError . NOT _FOUND ) return next ( new HttpError ( 401 , 'Username and password does not match' ) ) ;
2023-03-13 19:08:41 +01:00
if ( verifyError ) return next ( new HttpError ( 500 , verifyError ) ) ;
2023-05-12 14:31:26 +02:00
if ( ! user ) return next ( new HttpError ( 401 , 'Username and password does not match' ) ) ;
2023-03-10 17:13:33 +01:00
2023-03-13 19:08:41 +01:00
// TODO we may have to check what else the Account class provides, in which case we have to map those things
const result = {
login : {
accountId : user . id ,
} ,
} ;
2023-03-14 10:47:01 +01:00
const [ interactionFinishError , redirectTo ] = await safe ( provider . interactionResult ( req , res , result ) ) ;
2023-03-13 19:08:41 +01:00
if ( interactionFinishError ) return next ( new HttpError ( 500 , interactionFinishError ) ) ;
2023-08-26 08:18:58 +05:30
const auditSource = AuditSource . fromOidcRequest ( req ) ;
2023-07-25 18:02:39 +05:30
await eventlog . add ( user . ghost ? eventlog . ACTION _USER _LOGIN _GHOST : eventlog . ACTION _USER _LOGIN , auditSource , { userId : user . id , user : users . removePrivateFields ( user ) , appId : clientId } ) ;
2023-07-24 19:24:03 +02:00
if ( ! user . ghost ) safe ( users . notifyLoginLocation ( user , ip , userAgent , auditSource ) , { debug } ) ;
2023-07-25 12:22:54 +05:30
// debug(`route interaction login post result redirectTo:${redirectTo}`);
2023-03-14 10:47:01 +01:00
res . status ( 200 ) . send ( { redirectTo } ) ;
2023-03-14 14:19:29 +01:00
} ;
}
function interactionConfirm ( provider ) {
assert . strictEqual ( typeof provider , 'object' ) ;
2023-03-10 17:13:33 +01:00
2023-03-14 14:19:29 +01:00
return async function ( req , res , next ) {
2023-03-10 17:13:33 +01:00
try {
const interactionDetails = await provider . interactionDetails ( req , res ) ;
2023-04-25 13:13:04 +02:00
let { grantId , uid , prompt : { name , details } , params , session : { accountId } } = interactionDetails ;
2023-03-10 17:13:33 +01:00
2023-03-11 17:22:27 +01:00
debug ( ` route interaction confirm post uid: ${ uid } prompt.name: ${ name } accountId: ${ accountId } ` ) ;
2023-03-10 17:13:33 +01:00
assert . equal ( name , 'consent' ) ;
2023-04-25 13:13:04 +02:00
const client = await clientsGet ( params . client _id ) ;
2023-03-10 17:13:33 +01:00
2023-04-25 13:13:04 +02:00
// Check if user has access to the app if client refers to an app
// In most cases the user interaction already ends in the consent screen (see above)
if ( client . appId ) {
const app = await apps . get ( client . appId ) ;
const user = await users . get ( accountId ) ;
if ( ! apps . canAccess ( app , user ) ) {
const result = {
error : 'access_denied' ,
error _description : 'User has no access to this app' ,
} ;
return await provider . interactionFinished ( req , res , result , { mergeWithLastSubmission : false } ) ;
}
}
let grant ;
2023-03-10 17:13:33 +01:00
if ( grantId ) {
// we'll be modifying existing grant in existing session
grant = await provider . Grant . find ( grantId ) ;
} else {
// we're establishing a new grant
grant = new provider . Grant ( {
accountId ,
clientId : params . client _id ,
} ) ;
}
if ( details . missingOIDCScope ) {
grant . addOIDCScope ( details . missingOIDCScope . join ( ' ' ) ) ;
}
if ( details . missingOIDCClaims ) {
grant . addOIDCClaims ( details . missingOIDCClaims ) ;
}
if ( details . missingResourceScopes ) {
// eslint-disable-next-line no-restricted-syntax
for ( const [ indicator , scopes ] of Object . entries ( details . missingResourceScopes ) ) {
grant . addResourceScope ( indicator , scopes . join ( ' ' ) ) ;
}
}
grantId = await grant . save ( ) ;
const consent = { } ;
if ( ! interactionDetails . grantId ) {
// we don't have to pass grantId to consent, we're just modifying existing one
consent . grantId = grantId ;
}
const result = { consent } ;
await provider . interactionFinished ( req , res , result , { mergeWithLastSubmission : true } ) ;
} catch ( err ) {
next ( err ) ;
}
2023-03-14 14:19:29 +01:00
} ;
}
function interactionAbort ( provider ) {
assert . strictEqual ( typeof provider , 'object' ) ;
2023-03-10 17:13:33 +01:00
2023-03-14 14:19:29 +01:00
return async function ( req , res , next ) {
2023-03-10 17:13:33 +01:00
try {
const result = {
error : 'access_denied' ,
error _description : 'End-User aborted interaction' ,
} ;
await provider . interactionFinished ( req , res , result , { mergeWithLastSubmission : false } ) ;
} catch ( err ) {
next ( err ) ;
}
2023-03-14 14:19:29 +01:00
} ;
2023-03-10 17:13:33 +01:00
}
2023-03-14 12:24:35 +01:00
/ * *
* @ param use - can either be "id_token" or "userinfo" , depending on
* where the specific claims are intended to be put in .
* @ param scope - the intended scope , while oidc - provider will mask
* claims depending on the scope automatically you might want to skip
* loading some claims from external resources etc . based on this detail
* or not return them in id tokens but only userinfo and so on .
* /
2023-07-25 12:22:54 +05:30
async function claims ( userId /*, use, scope*/ ) {
2023-03-14 12:24:35 +01:00
const [ error , user ] = await safe ( users . get ( userId ) ) ;
if ( error ) return { error : 'user not found' } ;
const displayName = user . displayName || user . username || '' ; // displayName can be empty and username can be null
2024-02-06 16:43:05 +01:00
const { firstName , lastName , middleName } = users . parseDisplayName ( displayName ) ;
2023-03-14 12:24:35 +01:00
2024-01-29 13:55:31 +01:00
const { fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
2024-02-06 16:43:05 +01:00
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
2023-03-14 12:52:37 +01:00
const claims = {
2023-03-17 14:20:21 +01:00
sub : user . username , // it is essential to always return a sub claim
2023-03-14 12:24:35 +01:00
email : user . email ,
email _verified : true ,
family _name : lastName ,
2024-02-06 16:43:05 +01:00
middle _name : middleName ,
2023-03-14 12:24:35 +01:00
given _name : firstName ,
locale : 'en-US' ,
name : user . displayName ,
2024-01-29 13:55:31 +01:00
picture : ` https:// ${ dashboardFqdn } /api/v1/profile/avatar/ ${ user . id } ` ,
2023-03-14 12:52:37 +01:00
preferred _username : user . username
2023-03-14 12:24:35 +01:00
} ;
2023-03-14 12:52:37 +01:00
return claims ;
2023-03-14 12:24:35 +01:00
}
2023-03-21 15:12:55 +01:00
async function findAccount ( ctx , id ) {
return {
accountId : id ,
async claims ( use , scope ) { return await claims ( id , use , scope ) ; } ,
} ;
}
async function renderError ( ctx , out , error ) {
const data = {
2023-08-11 13:36:46 +02:00
errorMessage : error . error _description || error . error _detail || 'Unknown error' ,
footer : marked . parse ( await branding . renderFooter ( ) )
2023-03-21 15:12:55 +01:00
} ;
2023-04-16 10:49:59 +02:00
debug ( 'renderError: %o' , error ) ;
2023-03-21 15:12:55 +01:00
ctx . type = 'html' ;
ctx . body = ejs . render ( fs . readFileSync ( path . join ( _ _dirname , 'oidc_templates/error.ejs' ) , 'utf8' ) , data , { } ) ;
}
2023-03-21 14:39:58 +01:00
async function start ( ) {
2023-10-01 13:26:43 +05:30
assert ( gHttpServer === null , 'Already started' ) ;
2023-03-21 14:39:58 +01:00
const app = express ( ) ;
gHttpServer = http . createServer ( app ) ;
2023-03-10 17:13:33 +01:00
2023-05-11 15:26:02 +02:00
const Provider = ( await import ( 'oidc-provider' ) ) . default ;
2023-03-08 16:41:59 +01:00
2023-03-23 18:02:45 +01:00
// TODO we may want to rotate those in the future
2023-04-04 11:32:32 +02:00
const jwksKeys = [ ] ;
let keyEdDsa = await blobs . getString ( blobs . OIDC _KEY _EDDSA ) ;
if ( ! keyEdDsa ) {
2023-03-23 18:02:45 +01:00
debug ( 'Generating new OIDC EdDSA key' ) ;
const { privateKey } = await jose . generateKeyPair ( 'EdDSA' ) ;
2023-04-04 11:32:32 +02:00
keyEdDsa = await jose . exportJWK ( privateKey ) ;
await blobs . setString ( blobs . OIDC _KEY _EDDSA , JSON . stringify ( keyEdDsa ) ) ;
jwksKeys . push ( keyEdDsa ) ;
} else {
debug ( 'Using existing OIDC EdDSA key' ) ;
jwksKeys . push ( JSON . parse ( keyEdDsa ) ) ;
}
let keyRs256 = await blobs . getString ( blobs . OIDC _KEY _RS256 ) ;
if ( ! keyRs256 ) {
2023-04-04 15:38:45 +02:00
debug ( 'Generating new OIDC RS256 key' ) ;
2023-04-04 11:32:32 +02:00
const { privateKey } = await jose . generateKeyPair ( 'RS256' ) ;
keyRs256 = await jose . exportJWK ( privateKey ) ;
await blobs . setString ( blobs . OIDC _KEY _RS256 , JSON . stringify ( keyRs256 ) ) ;
jwksKeys . push ( keyRs256 ) ;
2023-03-23 18:02:45 +01:00
} else {
2023-04-04 15:38:45 +02:00
debug ( 'Using existing OIDC RS256 key' ) ;
2023-04-04 11:32:32 +02:00
jwksKeys . push ( JSON . parse ( keyRs256 ) ) ;
2023-03-23 18:02:45 +01:00
}
2023-08-02 20:01:29 +05:30
let cookieSecret = await settings . get ( settings . OIDC _COOKIE _SECRET _KEY ) ;
2023-07-25 12:36:32 +02:00
if ( ! cookieSecret ) {
debug ( 'Generating new cookie secret' ) ;
2024-01-23 12:44:23 +01:00
cookieSecret = crypto . randomBytes ( 256 ) . toString ( 'base64' ) ;
2023-08-02 20:01:29 +05:30
await settings . set ( settings . OIDC _COOKIE _SECRET _KEY , cookieSecret ) ;
2023-07-25 12:36:32 +02:00
}
2023-03-08 16:41:59 +01:00
const configuration = {
2023-03-21 15:12:55 +01:00
findAccount ,
renderError ,
2023-03-08 16:41:59 +01:00
adapter : CloudronAdapter ,
2023-03-09 20:17:27 +01:00
interactions : {
url : async function ( ctx , interaction ) {
2023-03-21 14:39:58 +01:00
return ` ${ ROUTE _PREFIX } /interaction/ ${ interaction . uid } ` ;
2023-03-09 20:17:27 +01:00
}
2023-03-10 16:07:45 +01:00
} ,
2023-03-23 18:02:45 +01:00
jwks : {
2023-04-04 11:32:32 +02:00
jwksKeys
2023-03-23 18:02:45 +01:00
} ,
2023-03-16 16:42:18 +01:00
claims : {
email : [ 'email' , 'email_verified' ] ,
profile : [ 'family_name' , 'given_name' , 'locale' , 'name' , 'preferred_username' ]
} ,
2023-03-11 17:22:27 +01:00
features : {
2024-04-04 10:41:00 +02:00
rpInitiatedLogout : { enabled : false } ,
2023-07-20 13:26:07 +02:00
devInteractions : { enabled : false }
2023-03-11 17:22:27 +01:00
} ,
2023-06-02 20:47:36 +02:00
responseTypes : [
'code' ,
'id_token' , 'id_token token' ,
'code id_token' , 'code token' , 'code id_token token' ,
'none' ,
] ,
2023-03-15 13:37:51 +01:00
// if a client only has one redirect uri specified, the client does not have to provide it in the request
allowOmittingSingleRegisteredRedirectUri : true ,
2023-03-14 14:58:09 +01:00
clients : [ ] ,
cookies : {
2023-07-25 12:36:32 +02:00
keys : [ cookieSecret ]
2023-03-14 14:58:09 +01:00
} ,
2023-03-08 16:41:59 +01:00
pkce : {
2023-04-30 10:18:39 +02:00
required : function pkceRequired ( /*ctx, client*/ ) {
2023-03-08 16:41:59 +01:00
return false ;
}
2023-03-14 10:47:01 +01:00
} ,
2024-04-11 15:51:20 +02:00
clientBasedCORS ( ctx , origin , client ) {
// allow CORS for clients where at least the origin matches where we redirect back to
if ( client . redirectUris . find ( ( u ) => u . indexOf ( origin ) === 0 ) ) return true ;
return false ;
} ,
2023-06-14 16:45:51 +02:00
conformIdTokenClaims : false ,
2023-06-04 16:29:55 +02:00
// https://github.com/panva/node-oidc-provider/blob/main/recipes/skip_consent.md
2023-06-04 13:42:28 +02:00
loadExistingGrant : async function ( ctx ) {
const grantId = ctx . oidc . result ? . consent ? . grantId
|| ctx . oidc . session . grantIdFor ( ctx . oidc . client . clientId ) ;
if ( grantId ) {
2023-07-25 12:22:54 +05:30
return await ctx . oidc . provider . Grant . find ( grantId ) ;
2023-08-07 16:53:00 +02:00
} else if ( ctx . oidc . client . clientId === tokens . ID _WEBADMIN || ctx . oidc . client . clientId === tokens . ID _DEVELOPMENT ) {
2023-06-04 16:29:55 +02:00
const grant = new ctx . oidc . provider . Grant ( {
clientId : ctx . oidc . client . clientId ,
accountId : ctx . oidc . session . accountId ,
} ) ;
2023-06-04 13:42:28 +02:00
2023-06-04 16:29:55 +02:00
grant . addOIDCScope ( 'openid email profile' ) ;
// grant.addOIDCClaims(['first_name']);
await grant . save ( ) ;
2023-06-04 13:42:28 +02:00
2023-06-04 16:29:55 +02:00
return grant ;
2023-06-04 13:42:28 +02:00
}
} ,
2023-03-14 10:47:01 +01:00
ttl : {
// in seconds, can also be a function returning the seconds https://github.com/panva/node-oidc-provider/blob/b1c1a9318036c2d3793cc9e668f99937c5c36bc6/docs/README.md#ttl
AccessToken : 3600 , // 1 hour
IdToken : 3600 , // 1 hour
Grant : 1209600 , // 14 days
Session : 1209600 , // 14 days
Interaction : 3600 // 1 hour
2023-03-08 16:41:59 +01:00
}
} ;
2023-08-14 11:08:38 +05:30
const { subdomain , domain } = await dashboard . getLocation ( ) ;
const fqdn = dns . fqdn ( subdomain , domain ) ;
debug ( ` start: create provider for ${ fqdn } at ${ ROUTE _PREFIX } ` ) ;
const provider = new Provider ( ` https:// ${ fqdn } ${ ROUTE _PREFIX } ` , configuration ) ;
2023-03-21 14:39:58 +01:00
app . enable ( 'trust proxy' ) ;
2023-03-21 14:46:09 +01:00
provider . proxy = true ;
2023-03-08 16:41:59 +01:00
2023-03-21 14:39:58 +01:00
app . set ( 'views' , path . join ( _ _dirname , 'oidc_templates' ) ) ;
app . set ( 'view engine' , 'ejs' ) ;
2023-03-21 13:54:40 +01:00
2023-03-21 14:46:09 +01:00
const json = middleware . json ( { strict : true , limit : '2mb' } ) ;
2023-03-21 14:39:58 +01:00
function setNoCache ( req , res , next ) {
res . set ( 'cache-control' , 'no-store' ) ;
next ( ) ;
}
2023-03-21 14:46:09 +01:00
app . get ( ` ${ ROUTE _PREFIX } /interaction/:uid ` , setNoCache , renderInteractionPage ( provider ) ) ;
app . post ( ` ${ ROUTE _PREFIX } /interaction/:uid/login ` , setNoCache , json , interactionLogin ( provider ) ) ;
app . post ( ` ${ ROUTE _PREFIX } /interaction/:uid/confirm ` , setNoCache , json , interactionConfirm ( provider ) ) ;
app . get ( ` ${ ROUTE _PREFIX } /interaction/:uid/abort ` , setNoCache , interactionAbort ( provider ) ) ;
2023-03-21 13:54:40 +01:00
2023-03-21 14:39:58 +01:00
app . use ( ROUTE _PREFIX , provider . callback ( ) ) ;
2023-05-12 14:31:26 +02:00
app . use ( middleware . lastMile ( ) ) ;
2023-03-21 14:39:58 +01:00
await util . promisify ( gHttpServer . listen . bind ( gHttpServer ) ) ( constants . OIDC _PORT , '127.0.0.1' ) ;
2023-03-21 13:54:40 +01:00
}
async function stop ( ) {
2023-03-21 14:39:58 +01:00
if ( ! gHttpServer ) return ;
await util . promisify ( gHttpServer . close . bind ( gHttpServer ) ) ( ) ;
gHttpServer = null ;
2023-03-21 13:54:40 +01:00
}