import { URL , URLSearchParams } from 'url' ; import { Config , Context , generateToken , HttpResponseRedirect } from '@foal/core' ; import * as fetch from 'node-fetch' ; export interface export interface SocialTokens { access_token : string ; token_type : string ; [ name ] : any ; } export interface template UserInfo export interface UserInfoAndTokens < UserInfo = any > { userInfo : UserInfo ; tokens : SocialTokens ; } export class extends Error export class InvalidStateError extends Error { readonly name = 'InvalidStateError' ; constructor ( ) { super ( 'Suspicious operation: the state of the callback does not match the state of the authorization request.' ) ; } } export class extends Error export class AuthorizationError extends Error { readonly name = 'AuthorizationError' ; constructor ( readonly error , readonly errorDescription ? , readonly errorUri ? ) { super ( 'The authorization server returned an error. Impossible to get an authorization code.\n' + `- error: ${ error }\n` + `- description: ${ errorDescription }\n` + `- URI: ${ errorUri }` ) ; } } export class extends Error export class TokenError extends Error { readonly name = 'TokenError' ; constructor ( readonly error ) { super ( 'The authorization server returned an error. Impossible to get an access token.\n' + JSON . stringify ( error , null , 2 ) ) ; } } const STATE_COOKIE_NAME = 'oauth2-state' ; export interface ObjectType { [ name ] : any ; } export abstract class template AuthParameters template UserInfoParameters export abstract class AbstractProvider < AuthParameters extends ObjectType , UserInfoParameters extends ObjectType > { protected abstract type { { clientId : string ; clientSecret : string ; redirectUri : string ; * } } memberof protected readonly abstract configPaths : { clientId : string ; clientSecret : string ; redirectUri : string ; } ; protected abstract type { string } memberof protected readonly abstract authEndpoint : string ; protected abstract type { string } memberof protected readonly abstract tokenEndpoint : string ; protected type { string [ ] } memberof protected readonly defaultScopes : string [ ] = [ ] ; protected type { string } memberof protected readonly scopeSeparator : string = ' ' ; private get config ( ) { return { clientId : Config . getOrThrow ( this . configPaths . clientId , 'string' ) , clientSecret : Config . getOrThrow ( this . configPaths . clientSecret , 'string' ) , redirectUri : Config . getOrThrow ( this . configPaths . redirectUri , 'string' ) , } ; } abstract param { SocialTokens } tokens param { UserInfoParameters } params returns { * } memberof abstract getUserInfoFromTokens ( tokens , params ? ) ; param { { scopes ? : string [ ] } } param { AuthParameters } params returns { Promise < HttpResponseRedirect > } memberof async redirect ( { scopes } = { } , params ? ) { const url = new URL ( this . authEndpoint ) ; url . searchParams . set ( 'response_type' , 'code' ) ; url . searchParams . set ( 'client_id' , this . config . clientId ) ; url . searchParams . set ( 'redirect_uri' , this . config . redirectUri ) ; const actualScopes = scopes || this . defaultScopes ; if ( actualScopes . length > 0 ) { url . searchParams . set ( 'scope' , actualScopes . join ( this . scopeSeparator ) ) ; } const state = await this . getState ( ) ; url . searchParams . set ( 'state' , state ) ; if ( params ) { for ( const key in params ) { url . searchParams . set ( key , params [ key ] ) ; } } return new HttpResponseRedirect ( url . href ) . setCookie ( STATE_COOKIE_NAME , state , { httpOnly : true , maxAge : 300 , path : '/' , secure : Config . get ( 'settings.social.cookie.secure' , 'boolean' , false ) } ) ; } param { Context } ctx returns { Promise < SocialTokens > } memberof async getTokens ( ctx ) { if ( ctx . request . query . state !== ctx . request . cookies [ STATE_COOKIE_NAME ] ) { throw new InvalidStateError ( ) ; } if ( ctx . request . query . error ) { throw new AuthorizationError ( ctx . request . query . error , ctx . request . query . error_description , ctx . request . query . error_uri ) ; } const params = new URLSearchParams ( ) ; params . set ( 'grant_type' , 'authorization_code' ) ; params . set ( 'code' , ctx . request . query . code || '' ) ; params . set ( 'redirect_uri' , this . config . redirectUri ) ; params . set ( 'client_id' , this . config . clientId ) ; params . set ( 'client_secret' , this . config . clientSecret ) ; const response = await fetch ( this . tokenEndpoint , { body : params , headers : { Accept : 'application/json' } , method : 'POST' , } ) ; const body = await response . json ( ) ; if ( ! response . ok ) { throw new TokenError ( body ) ; } return body ; } template UserInfo param { Context } ctx param { UserInfoParameters } params returns { Promise < UserInfoAndTokens < UserInfo > > } memberof async getUserInfo < UserInfo > ( ctx , params ? ) { const tokens = await this . getTokens ( ctx ) ; const userInfo = await this . getUserInfoFromTokens ( tokens , params ) ; return { userInfo , tokens } ; } private async getState ( ) { return generateToken ( ) ; } }