Source: jive-sdk-service/routes/oauth.js

/*
 * Copyright 2013 Jive Software
 *
 *    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.
 */

/**
 * @module oauthRoutes
 */

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

var http = require('http');
var url = require('url');
var jive = require('../api');
var mustache = require('mustache');
var q = require('q');
var util = require("util");

exports.redirectHtmlTxt = "<html> <head> <script> window.location='{{{redirect}}}'; </script>" +
    "</head> <body> Redirecting ... </body> </html>";

exports.fetchOAuth2Conf = function() {
    return jive.service.options['oauth2'];
};

function getOAuth2Conf(targetJiveTenantID, originJiveTenantID, self) {
    var deferred = q.defer();

    if ( targetJiveTenantID ) {
        jive.community.findByTenantID(targetJiveTenantID).then( function( community ) {
            if ( community ) {
                try{
                    var oauth2Conf = JSON.parse( JSON.stringify(self.fetchOAuth2Conf( {
                        'originJiveTenantID' : originJiveTenantID,
                        'targetJiveTenantID' : targetJiveTenantID
                    }) || {} ) );
                } catch ( e ) {
                    deferred.reject(e);
                    return;
                }
                oauth2Conf['oauth2ConsumerKey'] = community['clientId'];
                oauth2Conf['oauth2ConsumerSecret'] = community['clientSecret'];
                oauth2Conf['originServerAuthorizationUrl'] = community['jiveUrl'] + '/oauth2/authorize';
                oauth2Conf['originServerTokenRequestUrl'] = community['jiveUrl'] + '/oauth2/token';

                deferred.resolve( oauth2Conf );
            } else {
                deferred.reject(new Error("Could not find community for tenantID " + targetJiveTenantID ));
            }
        });

    } else {
        deferred.resolve( self.fetchOAuth2Conf({
            'originJiveTenantID' : originJiveTenantID,
            'targetJiveTenantID' : targetJiveTenantID
        }) );
    }

    return deferred.promise;
}

/**
 * <h5>Route:</h5>
 * <i>GET /authorizeUrl</i>
 * <br><br>
 * Expects:
 * - viewerID
 * - callback
 * - base64 encoded Authorization header
 * @param req
 * @param res
 */
exports.authorizeUrl = function(req, res ) {
    var url_parts = url.parse(req.url, true);
    var query = url_parts.query;

    var viewerID = query['viewerID'];
    var callback = query['callback'];
    var targetJiveTargetID = query['jiveTenantID'];
    var jiveExtensionHeaders = jive.util.request.parseJiveExtensionHeaders(req);
    if (jiveExtensionHeaders ) {
        var originJiveTenantID = jiveExtensionHeaders['tenantID'];
        jive.logger.debug('Origin jive tenantID', originJiveTenantID);
    }

    var contextStr = query['context'];
    if ( contextStr ) {
        try {
            var context = JSON.parse( decodeURI(contextStr) );
        } catch (e) {
            errorResponse( res, 400, 'Invalid context string, could not parse');
            return;
        }
    }

    // encode the target jiveTenantID in the context
    if ( targetJiveTargetID ) {
        context = context || {};
        context = { 'jiveTenantID' : targetJiveTargetID };
    }

    // encode the origin jiveTenantID in the context
    if ( originJiveTenantID ) {
        context = context || {};
        context['originJiveTenantID'] = originJiveTenantID;
    }

    var extraAuthParamsStr = query['extraAuthParams'];
    if ( extraAuthParamsStr ) {
        try {
            var extraAuthParams = JSON.parse( decodeURI(extraAuthParamsStr ));
        } catch (e) {
            errorResponse( res, 400, 'Invalid extra auth param string, could not parse');
            return;
        }
    }
    var self = this;
    getOAuth2Conf(targetJiveTargetID, originJiveTenantID, this ).then( function(oauth2Conf) {
        jive.logger.info(JSON.stringify(oauth2Conf, null, 4));
        var responseMap = self.buildAuthorizeUrlResponseMap(
            oauth2Conf, callback, { 'viewerID': viewerID, 'context': context}, extraAuthParams );

        jive.logger.debug('Sending', responseMap);

        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end( JSON.stringify(responseMap) );
    }, function(err) {
        errorResponse( res, 404, err);
    });
};

/**
 * Override this in a subclass!
 * @param oauth2CallbackRequest
 * @param originServerAccessTokenResponse
 * @param callback
 */
exports.oauth2SuccessCallback = function( state, originServerAccessTokenResponse, callback ) {
    jive.logger.debug('State', state);
    jive.logger.debug('Origin server Access Token Response', originServerAccessTokenResponse);
    callback();
};

var errorResponse = function( res, code, error ){
    res.status(code);
    res.set({'Content-Type': 'application/json'});
    var err = {'error':error};
    res.send( JSON.stringify(err) );
    jive.logger.debug(err);
};

/**
 * <h4><i>POST /oauth2Callback</i></h4>
 * <br>
 * Expects:
 * - code
 * - state, which is a base64 encoded JSON structure containing at minimum jiveRedirectUrl attribute
 * @param req
 * @param res
 */
exports.oauth2Callback = function(req, res ) {
    var url_parts = url.parse(req.url, true);
    var query = url_parts.query;

    var code = query['code'];
    if ( !code ) {
        errorResponse( res, 400, 'Authorization code required');
        return;
    }

    var stateStr = query['state'];
    if ( !stateStr ) {
        errorResponse( res, 400, 'Missing state string');
        return;
    }

    try {
        var state = JSON.parse( JSON.parse( jive.util.base64Decode(stateStr)) );
    } catch ( e ) {
        errorResponse( res, 400, 'Invalid state string, cannot parse.');
        return;
    }

    var jiveRedirectUrl = state['jiveRedirectUrl'];
    if ( !jiveRedirectUrl ) {
        errorResponse( res, 400, 'Invalid state string, no jiveRedirectUrl provided.');
        return;
    }

    var jiveTenantID = state['context'] ? state['context']['jiveTenantID'] : undefined;
    var originJiveTenantID = state['context'] ? state['context']['originJiveTenantID'] : undefined;
    var self = this;

    getOAuth2Conf(jiveTenantID, originJiveTenantID, this).then(

        /////////////
        function(oauth2Conf) {

            var oauth2CallbackExtraParams = oauth2Conf['oauth2CallbackExtraParams'];

            var postObject = self.buildOauth2CallbackObject( oauth2Conf, code, oauth2CallbackExtraParams );
            jive.logger.debug("Post object", postObject);

            var headers = { 'Content-Type': 'application/x-www-form-urlencoded' };

            var proceed = function(context) {
                var redirectParams = '';

                if ( context && typeof context === 'object' ) {
                    for (var key in context) {
                        if (context.hasOwnProperty(key)) {
                            if (redirectParams.length > 0) {
                                redirectParams += '&';
                            }
                            redirectParams += encodeURIComponent(key) + '=' + encodeURIComponent(context[key]);
                        }
                    }
                }

                var redirect = decodeURIComponent(jiveRedirectUrl) + ( redirectParams ? '?' : '') + redirectParams;
                var redirectHtml = mustache.render( self.redirectHtmlTxt, { 'redirect' : redirect } );

                res.status(200);
                res.set({'Content-Type': 'text/html'});
                res.send(redirectHtml);
            };

            var oauth2SuccessCallback = self.oauth2SuccessCallback;

            jive.util.buildRequest( oauth2Conf['originServerTokenRequestUrl'], 'POST', postObject, headers).then(
                function(response) {
                    // success
                    if ( response.statusCode >= 200 && response.statusCode < 299 ) {
                        if (oauth2SuccessCallback) {
                            oauth2SuccessCallback( state, response, proceed );
                        } else {
                            proceed();
                        }
                    } else {
                        res.status(response.statusCode);
                        res.set({'Content-Type': 'application/json'});
                        res.send(response.entity);
                    }
                },
                function(e) {
                    // failure
                    errorResponse( res, 500, e);
                }
            );


        },

        /////////////
        function(err) {
            errorResponse( res, 500, err);
        }
    );

};

exports.buildAuthorizeUrlResponseMap = function (oauth2Conf, callback, context, extraAuthParams) {
    var stateToEncode = { 'jiveRedirectUrl': callback };
    if (context) {
        stateToEncode = util._extend(stateToEncode, context);
    }

    var url = oauth2Conf['originServerAuthorizationUrl'] + "?" +
        "state=" + jive.util.base64Encode(JSON.stringify(stateToEncode)) +
        "&redirect_uri=" + oauth2Conf['clientOAuth2CallbackUrl'] +
        "&client_id=" + oauth2Conf['oauth2ConsumerKey'] +
        "&response_type=" + "code";

    if (extraAuthParams) {
        var extraAuthStr = '';
        for (var key in extraAuthParams) {
            if (extraAuthParams.hasOwnProperty(key)) {
                extraAuthStr += '&' + key + '=' + extraAuthParams[key];
            }
        }

        url += extraAuthStr;
    }

    return {
        'url': url
    };
};

exports.buildOauth2CallbackObject = function (oauth2Conf, code, extraParams) {
    var postObject = {
        'grant_type': 'authorization_code',
        'redirect_uri': oauth2Conf['clientOAuth2CallbackUrl'],
        'client_id': oauth2Conf['oauth2ConsumerKey'],
        'client_secret': oauth2Conf['oauth2ConsumerSecret'],
        'code': code
    };

    if (extraParams) {
        postObject = util._extend(postObject, extraParams);
    }

    return postObject;
};