CocoonIO/cocoon-cloud-sdk

View on GitHub
src/lib/oauth.ts

Summary

Maintainability
B
5 hrs
Test Coverage
"use strict";

import {plugins, Response} from "popsicle";

import CocoonAPI from "./cocoon-api";
import {GrantType} from "./enums/e-grant-type";
import {IAccessToken} from "./interfaces/i-access-token";

export default class OAuth {
    private static generateRandomString(length: number = 16): string {
        let text = "";
        const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        for (let i = 0; i < length; i++) {
            text += possible.charAt(Math.floor(Math.random() * possible.length));
        }
        return text;
    }

    private readonly clientId: string;
    private readonly clientSecret: string;
    private readonly grantType: GrantType | string;
    private readonly oAuthURL: string;
    private readonly redirectURI: string;
    private readonly ACCESS_TOKEN = "access_token";
    private readonly AUTHORIZATION = "login";
    private readonly LOGOUT = "logout";

    private state: string;

    public constructor(
        grantType: GrantType | string,
        clientId: string,
        clientSecret?: string,
        redirectURI?: string,
        oAuthURL: string = "https://cloud.cocoon.io/oauth/",
    ) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.grantType = grantType;
        this.oAuthURL = oAuthURL;
        this.redirectURI = redirectURI;
    }

    public get accessTokenURL(): string {
        return this.oAuthURL + this.ACCESS_TOKEN;
    }

    public get authorizationURL(): string {
        return this.oAuthURL + this.AUTHORIZATION;
    }

    public get logoutURL(): string {
        return this.oAuthURL.replace("oauth", this.LOGOUT);
    }

    /**
     * Generate the URL where the user can authorize the application following the Authorization Code OAuth flow.
     * @param scope Scope of the access the application requests.
     * @returns {string} The URL where the user can authorize the application.
     */
    public authorizeAuthorizationCode(scope?: string): string {
        if (this.grantType === GrantType.AuthorizationCode) {
            this.state = OAuth.generateRandomString();
            return (
                this.authorizationURL +
                "?response_type=" +
                "code" +
                "&client_id=" +
                this.clientId +
                "&redirect_uri=" +
                this.redirectURI +
                (scope ? "&scope=" + scope : "") +
                "&state=" +
                this.state
            );
        } else {
            console.error("Grant Type is " + this.grantType + " when it should be " + GrantType.AuthorizationCode);
            throw new Error("Invalid OAuth flow");
        }
    }

    /**
     * Exchange the code you got after the user authorized the application for the access token following
     * the Authorization Code OAuth flow.
     * @param pCode Code found in the redirect URL, as a parameter, after the user authorizes the application.
     * @param state Random string present in the redirect URL, as a parameter, after the user authorizes the application.
     * This object created it when generating the authorization URL.
     * The server should have returned the same string and will be tested now.
     * @returns {Promise<IAccessToken>} Promise of the access token.
     */
    public async tokenExchangeAuthorizationCode(pCode: string, state: string): Promise<IAccessToken> {
        const parameters = {
            client_id: this.clientId,
            client_secret: this.clientSecret,
            code: pCode,
            grant_type: this.grantType,
            redirect_uri: this.redirectURI,
        };

        if (this.grantType === GrantType.AuthorizationCode && this.isStateValid(state)) {
            if (this.grantType !== GrantType.AuthorizationCode) {
                throw new Error("Grant Type is " + this.grantType + " when it should be " + GrantType.AuthorizationCode);
            }
            if (!this.isStateValid(state)) {
                throw new Error("State is " + state + " when it should be " + this.state);
            }
        }

        return (await CocoonAPI.request(
            {
                body: parameters,
                headers: {"Content-Type": "application/x-www-form-urlencoded"},
                method: "POST",
                url: this.accessTokenURL,
            },
            [plugins.parse("json")],
            false,
        )).body;
    }

    public tokenExchangeClientCredentials() {
        console.warn("Access with Client Credentials not available yet");
        // const request = CocoonAPI.request({
        //     method: "POST",
        //     url: this.accessTokenURL
        //     + "?grant_type=" + this.grantType
        //     + "&client_id=" + this.clientId
        //     + "&client_secret=" + this.clientSecret,
        // }, false)
        // .use(plugins.parse("json"));
        //
        // if (this.grantType !== GrantType.ClientCredentials) {
        //     console.error("Grant Type is " + this.grantType + " when it should be " + GrantType.ClientCredentials);
        //     request.abort();
        // }
        // return request;
    }

    /**
     * Generate the URL where the user can authorize the application following the Implicit OAuth flow.
     * @param scope Scope of the access the application requests.
     * @returns {string} The URL where the user can authorize the application.
     */
    public authorizeImplicit(scope?: string): string {
        if (this.grantType === GrantType.Implicit) {
            return (
                this.authorizationURL +
                "?response_type=" +
                "token" +
                "&client_id=" +
                this.clientId +
                "&redirect_uri=" +
                this.redirectURI +
                (scope ? "&scope=" + scope : "")
            );
        } else {
            console.error("Grant Type is " + this.grantType + " when it should be " + GrantType.Implicit);
            throw new Error("Invalid OAuth flow");
        }
    }

    /**
     * Exchange a username and password for the access token following the Password OAuth flow.
     * @param pUsername Username of a user.
     * @param pPassword Password of a user.
     * @returns {Promise<IAccessToken>} Promise of the access token.
     */
    public async tokenExchangePassword(pUsername: string, pPassword: string): Promise<IAccessToken> {
        const parameters = {
            client_id: this.clientId,
            client_secret: this.clientSecret, // FIXME: Password flow should't need clientSecret
            grant_type: this.grantType,
            password: pPassword,
            username: pUsername,
        };

        if (this.grantType !== GrantType.Password) {
            throw new Error("Grant Type is " + this.grantType + " when it should be " + GrantType.Password);
        }

        return (await CocoonAPI.request(
            {
                body: parameters,
                headers: {"Content-Type": "application/x-www-form-urlencoded"},
                method: "POST",
                url: this.accessTokenURL,
            },
            [plugins.parse("json")],
            false,
        )).body;
    }

    /**
     * Log out of the API.
     * @returns {Promise<Response>} Promise of a successful logout.
     */
    public logout(): Promise<Response> {
        return CocoonAPI.request({
            method: "GET",
            url: this.logoutURL,
        });
    }

    private isStateValid(state: string): boolean {
        return this.state === state;
    }
}