@delewis13/appauth
Version:
A general purpose OAuth client. Vendored awaiting PR merge
154 lines (137 loc) • 6.18 kB
text/typescript
/*
* 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');
export class ServerEventsEmitter extends EventEmitter {
static ON_UNABLE_TO_START = 'unable_to_start';
static ON_AUTHORIZATION_RESPONSE = 'authorization_response';
static START_SERVER_SHUTDOWN = 'start_server_shutdown'
static SERVER_SHUTDOWN_COMPLETE = 'server_shutdown_complete'
static SERVER_ERROR_DURING_SHUTDOWN = 'server_error_during_shutdown';
}
export class NodeBasedHandler extends AuthorizationRequestHandler {
// the handle to the current authorization request
authorizationPromise: Promise<AuthorizationRequestResponse|null>|null = null;
public emitter = new ServerEventsEmitter()
public server: Http.Server | undefined
constructor(
// default to port 8000
public httpServerPort = 8000,
public openCallback: null | ((url: string) => void) = null,
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 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;
this.emitter.emit(ServerEventsEmitter.ON_AUTHORIZATION_RESPONSE, completeResponse);
response.writeHead(301, { Location: 'https://dungeon-dj.com/oauth'});
response.end();
};
this.authorizationPromise = new Promise<AuthorizationRequestResponse>((resolve, reject) => {
this.emitter.once(ServerEventsEmitter.ON_UNABLE_TO_START, () => {
reject(`Unable to create HTTP server at port ${this.httpServerPort}`);
});
this.emitter.once(ServerEventsEmitter.ON_AUTHORIZATION_RESPONSE, (result: any) => {
this.closeServer();
// resolve pending promise
resolve(result as AuthorizationRequestResponse);
// complete authorization flow
this.completeAuthorizationRequestIfPossible();
});
});
request.setupCodeVerifier()
.then(() => {
this.server = Http.createServer(requestHandler);
this.server.listen(this.httpServerPort).on("error", () => {
this.emitter.emit(ServerEventsEmitter.ON_UNABLE_TO_START);
});
this.emitter.on(ServerEventsEmitter.START_SERVER_SHUTDOWN, this.closeServer)
const url = this.buildRequestUrl(configuration, request);
log('Making a request to ', request, url);
if (this.openCallback) {
this.openCallback(url);
} else {
opener(url);
};
})
.catch((error) => {
log('Something bad happened ', error);
this.emitter.emit(ServerEventsEmitter.ON_UNABLE_TO_START);
});
}
public closeServer(callback?: () => void) {
this.server?.close((err) => {
if (err && !err.message.includes("Server is not running")) {
this.emitter.emit(ServerEventsEmitter.SERVER_ERROR_DURING_SHUTDOWN)
return
}
this.emitter.emit(ServerEventsEmitter.SERVER_SHUTDOWN_COMPLETE)
this.server = undefined
callback?.()
})
}
protected completeAuthorizationRequest(): Promise<AuthorizationRequestResponse|null> {
if (!this.authorizationPromise) {
return Promise.reject(
'No pending authorization request. Call performAuthorizationRequest() ?');
}
return this.authorizationPromise;
}
}