/**
 * Copyright (c) 2020, 2021 Oracle and/or its affiliates.  All rights reserved.
 * This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
 */ import UrlParser = require ( "url" ) ; const httpSignature = require ( "http-signature" ) ; import jssha = require ( "jssha" ) ; import FederationClient from "./models/federation-client" ; import SessionKeySupplier from "./models/session-key-supplier" ; import X509CertificateSupplier from "./models/X509-certificate-supplier" ; import SecurityTokenAdapter from "./security-token-adapter" ; import AuthUtils from "./helpers/auth-utils" ; import { RequestSigner } from "../signer" ; import { HttpRequest } from "../http-request" ; import { SignerRequest } from "../signer" ; import { FetchHttpClient } from "../http" ; import { PrivateKey } from "sshpk" ; import { getStringFromRequestBody } from "../helper" ; import CircuitBreaker from "../circuit-breaker" ; /**
 * This class gets a security token from the auth service by signing the request with a PKI issued leaf certificate,
 * passing along a temporary public key that is bounded to the the security token, and the leaf certificate.
 */ export default class X509FederationClient implements FederationClient { securityTokenAdapter : SecurityTokenAdapter ; private retry = 0 ; constructor ( private federationEndpoint , private _tenancyId , private _leafCertificateSupplier , private sessionKeySupplier , private intermediateCertificateSuppliers , private purpose ) { this . securityTokenAdapter = new SecurityTokenAdapter ( "" , this . sessionKeySupplier ) ; } get tenancyId ( ) : string { return this . _tenancyId ; } get leafCertificateSupplier ( ) : X509CertificateSupplier { return this . _leafCertificateSupplier ; } return throws async getSecurityToken ( ) { if ( this . securityTokenAdapter . isValid ( ) ) { return this . securityTokenAdapter . getSecurityToken ( ) ; } return await this . refreshAndGetSecurityTokenInner ( true ) ; } param key return async getStringClaim ( key ) { await this . refreshAndGetSecurityTokenInner ( true ) ; return this . securityTokenAdapter . getStringClaim ( key ) ; } async refreshAndGetSecurityToken ( ) { return await this . refreshAndGetSecurityTokenInner ( false ) ; } private async refreshAndGetSecurityTokenInner ( doFinalTokenValidityCheck ) { if ( ! doFinalTokenValidityCheck || ! this . securityTokenAdapter . isValid ( ) ) { this . sessionKeySupplier . refreshKeys ( ) ; if ( this . _leafCertificateSupplier && this . _leafCertificateSupplier . refresh ) { try { this . _leafCertificateSupplier = await this . _leafCertificateSupplier . refresh ( ) ; } catch ( e ) { throw Error ( `Fail to refresh leafCertificateSupplier, error: ${ e }` ) ; } if ( this . purpose === "DEFAULT" ) { const newTenancyId = AuthUtils . getTenantIdFromCertificate ( this . _leafCertificateSupplier . getCertificateAndKeyPair ( ) . getCertificate ( ) ) ; if ( this . tenancyId != newTenancyId ) { throw Error ( "The tenancy id should never be changed in cert file!" ) ; } } } for ( let i = 0 ; i < this . intermediateCertificateSuppliers . length ; i ++ ) { try { if ( this . intermediateCertificateSuppliers [ i ] . refresh ) { this . intermediateCertificateSuppliers [ i ] = await this . intermediateCertificateSuppliers [ i ] . refresh ! ( ) ; } } catch ( e ) { throw Error ( "Cannot refresh the intermediate certification" ) ; } } this . securityTokenAdapter = await this . getSecurityTokenFromServer ( ) ; return this . securityTokenAdapter . getSecurityToken ( ) ; } return this . securityTokenAdapter . getSecurityToken ( ) ; } return private async getSecurityTokenFromServer ( ) { const keyPair = this . sessionKeySupplier . getKeyPair ( ) ; if ( ! keyPair ) { throw Error ( "keyPair for session was not provided" ) ; } const publicKey = keyPair . getPublic ( ) ; if ( ! publicKey ) { throw Error ( "Public key is not present" ) ; } const certificateAndKeyPair = this . leafCertificateSupplier . getCertificateAndKeyPair ( ) ; if ( ! certificateAndKeyPair ) { throw Error ( "Certificate and key pair are not present" ) ; } const leafCertificate = certificateAndKeyPair . getCertificate ( ) ; if ( ! leafCertificate ) { throw Error ( "Leaf certificate is not present" ) ; } if ( ! certificateAndKeyPair . getPrivateKey ( ) ) { throw Error ( "Leaf certificate's private key is not present" ) ; } try { let intermediateStrings = [ ] ; if ( this . intermediateCertificateSuppliers && this . intermediateCertificateSuppliers . length > 0 ) { this . intermediateCertificateSuppliers . forEach ( ( supplier ) => { const supplierCertificateAndKeyPair = supplier . getCertificateAndKeyPair ( ) ; if ( supplierCertificateAndKeyPair && supplierCertificateAndKeyPair . getCertificate ( ) ) { intermediateStrings . push ( AuthUtils . sanitizeCertificateString ( supplierCertificateAndKeyPair . getCertificate ( ) . toString ( "pem" ) ) ) ; } } ) ; } const url = this . federationEndpoint + "/v1/x509" ; const requestPayload = { certificate : AuthUtils . sanitizeCertificateString ( leafCertificate . toString ( "pem" ) ) , purpose : this . purpose , publicKey : AuthUtils . sanitizeCertificateString ( publicKey ) , intermediateCertificates : intermediateStrings } ; let jsonPayload = JSON . stringify ( requestPayload ) ; jsonPayload = jsonPayload . replace ( /\\n/g , "" ) ; const requestObj = { uri : url , body : jsonPayload , method : "POST" , headers : new Headers ( ) } ; const fingerprint = leafCertificate . fingerprint ( "sha1" ) . toString ( "hex" ) ; const privateKey = certificateAndKeyPair . getPrivateKey ( ) as PrivateKey ; const signer = new AuthTokenRequestSigner ( this . tenancyId , fingerprint , privateKey ) ; const httpClient = new FetchHttpClient ( signer , CircuitBreaker . internalCircuit ) ; const response = await httpClient . send ( requestObj ) ; if ( response . status !== 200 ) { if ( this . retry < 3 ) { this . retry += 1 ; return await this . getSecurityTokenFromServer ( ) ; } else { throw Error ( `Failed to call auth service for token, error: ${ response }` ) ; } } this . retry = 0 ; const securityToken = await response . json ( ) ; return new SecurityTokenAdapter ( securityToken . token , this . sessionKeySupplier ) ; } catch ( e ) { throw Error ( `Failed to call call Auth service, error: ${ e }` ) ; } } } class AuthTokenRequestSigner implements RequestSigner { constructor ( public tenancyId , public fingerprint , public privateKey ) { } async signHttpRequest ( request , forceExcludeBody = false ) { const apiKey = `${ this . tenancyId }/fed-x509/${ this . fingerprint }` ; const headersToSign = [ "date" , "(request-target)" , "host" , "content-type" , "content-length" , "x-content-sha256" ] ; const EMPTY_SHA = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" ; if ( ! request . headers . has ( "host" ) ) { const url = UrlParser . parse ( request . uri ) ; if ( url . host ) { request . headers . set ( "host" , url . host ) ; } else { throw new Error ( "Cannot parse host from url" ) ; } } if ( ! request . headers . has ( "x-date" ) ) { request . headers . set ( "x-date" , new Date ( ) . toUTCString ( ) ) ; } if ( ! request . headers . has ( "Content-Type" ) ) { request . headers . set ( "Content-Type" , "application/json" ) ; } let contentLen = 0 ; const shaObj = new jssha ( "SHA-256" , "TEXT" ) ; if ( request . body ) { const bodyStringContent = await getStringFromRequestBody ( request . body ) ; shaObj . update ( bodyStringContent ) ; request . headers . set ( "x-content-sha256" , shaObj . getHash ( "B64" ) ) ; contentLen = bodyStringContent . length ; } if ( contentLen === 0 ) { request . headers . set ( "x-content-sha256" , EMPTY_SHA ) ; } if ( ! request . headers . has ( "Content-Length" ) ) { request . headers . set ( "Content-Length" , `${ contentLen }` ) ; } httpSignature . sign ( new SignerRequest ( request . method , request . uri , request . headers ) , { key : this . privateKey . toBuffer ( "pem" , { } ) , keyId : apiKey , headers : headersToSign } ) ; const authorizationHeader = request . headers . get ( "authorization" ) ; if ( authorizationHeader ) { request . headers . set ( "authorization" , authorizationHeader . replace ( "Signature " , 'Signature version="1",' ) ) ; } else { throw new Error ( "Unable to sign request" ) ; } } }