UNPKG

@ariyana/appauth

Version:

A general purpose OAuth client.

139 lines (121 loc) 5.79 kB
/* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * 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. */ import {EventEmitter} from 'events'; import * as Http from 'http'; import * as Url from 'url'; import {AuthorizationRequest} from '../authorization_request'; import {AuthorizationRequestHandler, AuthorizationRequestResponse} from '../authorization_request_handler'; import {AuthorizationError, AuthorizationResponse} from '../authorization_response'; import {AuthorizationServiceConfiguration} from '../authorization_service_configuration'; import {Crypto} from '../crypto_utils'; import {log} from '../logger'; import {BasicQueryStringUtils, QueryStringUtils} from '../query_string_utils'; import {NodeCrypto} from './crypto_utils'; // TypeScript typings for `opener` are not correct and do not export it as module import opener = require('opener'); class ServerEventsEmitter extends EventEmitter { static ON_UNABLE_TO_START = 'unable_to_start'; static ON_AUTHORIZATION_RESPONSE = 'authorization_response'; } export class NodeBasedHandler extends AuthorizationRequestHandler { // the handle to the current authorization request authorizationPromise: Promise<AuthorizationRequestResponse|null>|null = null; /** The content for the authorization redirect response page. */ protected authorizationRedirectPageContent = 'Close your browser to continue'; constructor( // default to port 8000 public httpServerPort = 8000, utils: QueryStringUtils = new BasicQueryStringUtils(), crypto: Crypto = new NodeCrypto()) { super(utils, crypto); } performAuthorizationRequest( configuration: AuthorizationServiceConfiguration, request: AuthorizationRequest) { // use opener to launch a web browser and start the authorization flow. // start a web server to handle the authorization response. const emitter = new ServerEventsEmitter(); const requestHandler = (httpRequest: Http.IncomingMessage, response: Http.ServerResponse) => { if (!httpRequest.url) { return; } const url = Url.parse(httpRequest.url); const searchParams = new Url.URLSearchParams(url.query || ''); const state = searchParams.get('state') || undefined; const code = searchParams.get('code'); const error = searchParams.get('error'); if (!state && !code && !error) { // ignore irrelevant requests (e.g. favicon.ico) return; } log('Handling Authorization Request ', searchParams, state, code, error); let authorizationResponse: AuthorizationResponse|null = null; let authorizationError: AuthorizationError|null = null; if (error) { log('error'); // get additional optional info. const errorUri = searchParams.get('error_uri') || undefined; const errorDescription = searchParams.get('error_description') || undefined; authorizationError = new AuthorizationError( {error: error, error_description: errorDescription, error_uri: errorUri, state: state}); } else { authorizationResponse = new AuthorizationResponse({code: code!, state: state!}); } const completeResponse = { request, response: authorizationResponse, error: authorizationError } as AuthorizationRequestResponse; emitter.emit(ServerEventsEmitter.ON_AUTHORIZATION_RESPONSE, completeResponse); response.setHeader('Content-Type', 'text/html'); response.end(this.authorizationRedirectPageContent); }; this.authorizationPromise = new Promise<AuthorizationRequestResponse>((resolve, reject) => { emitter.once(ServerEventsEmitter.ON_UNABLE_TO_START, () => { reject(`Unable to create HTTP server at port ${this.httpServerPort}`); }); emitter.once(ServerEventsEmitter.ON_AUTHORIZATION_RESPONSE, (result: any) => { // Set timeout for the server connections to 1 ms as we wish to close and end the server // as soon as possible. This prevents a user failing to close the redirect window from // causing a hanging process due to the server. server.setTimeout(1); server.close(); // resolve pending promise resolve(result as AuthorizationRequestResponse); // complete authorization flow this.completeAuthorizationRequestIfPossible(); }); }); let server: Http.Server; request.setupCodeVerifier() .then(() => { server = Http.createServer(requestHandler); server.listen(this.httpServerPort); const url = this.buildRequestUrl(configuration, request); log('Making a request to ', request, url); opener(url); }) .catch((error) => { log('Something bad happened ', error); emitter.emit(ServerEventsEmitter.ON_UNABLE_TO_START); }); } protected completeAuthorizationRequest(): Promise<AuthorizationRequestResponse|null> { if (!this.authorizationPromise) { return Promise.reject( 'No pending authorization request. Call performAuthorizationRequest() ?'); } return this.authorizationPromise; } }