oaeproject/Hilary

View on GitHub
packages/oae-authentication/lib/strategies/oauth/rest.js

Summary

Maintainability
C
1 day
Test Coverage
A
97%
/*!
 * Copyright 2014 Apereo Foundation (AF) Licensed under the
 * Educational Community 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://opensource.org/licenses/ECL-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 { BasicStrategy } from 'passport-http';
import OAuthPassport from 'passport-oauth2-client-password';
import oauth2orize from 'oauth2orize';
import passport from 'passport';

import { logger } from 'oae-logger';

import * as OAE from 'oae-util/lib/oae.js';
import * as OaeServer from 'oae-util/lib/server.js';

import * as OAuthDAO from './internal/dao.js';
import * as OAuthAPI from './api.js';

const ClientPasswordStrategy = OAuthPassport.Strategy;
const log = logger('oae-authentication');

/// //////////////
// OAuth setup //
/// //////////////

// The OAuth instance that will take care of creating authorization codes and access tokens
// As we're only use the Client Credentials Grant, there is no need yet to add client (de)serializers
const server = oauth2orize.createServer();

/// ///////////////////////////
// Client Credentials Grant //
/// ///////////////////////////

/*!
 * The following exchange is called the "Client Credentials Grant". In it, a client
 * exchanges its id and secret for an access token. Typically this is only used to update
 * the client's information. However, within OAE all API requests need to be either anonymous
 * or authenticated as a user. We deviate from the specification slightly and will bind each
 * client to a user. When a client requests an access token via the Client Credentials Grant,
 * we will grant an access token for the user who "owns" the client.
 *
 * By the time this method gets called, the client credentials have already been validated
 * and we can simply return an "Access Token".
 *
 * @param  {Client}         client              The client who desires an access token
 * @param  {[type]}         [scope]             The scope of access requested by the client. This is unused within OAE
 * @param  {Function}       callback            Standard callback function
 * @param  {Object}         callback.err        An error that occurred, if any
 * @param  {AccessToken}    callback.token      An access token that can be used to interact with the OAE apis as a user
 */
server.exchange(
  oauth2orize.exchange.clientCredentials(
    { userProperty: 'oaeAuthInfo' },
    (client, scope, callback) => {
      // In theory, each client should cache their access token, but that's probably a pipedream
      // We should check if this client has a token already so we don't generate a new one each time
      OAuthDAO.AccessTokens.getAccessTokenForUserAndClient(
        client.userId,
        client.id,
        (error, accessToken) => {
          if (error) {
            return callback(error);

            // This client has a token, return it
          }

          if (accessToken) {
            return callback(null, accessToken.token);
          }

          // This is the first time this client is requesting a token, we'll need to generate one
          const token = OAuthAPI.generateToken(256);
          OAuthDAO.AccessTokens.createAccessToken(token, client.userId, client.id, (error_) => {
            if (error_) {
              return callback(error_);
            }

            // Return an access token to the client
            log().info(
              { client: client.id, user: client.userId },
              'An access token has been handed out via Client Credentials'
            );
            return callback(null, token);
          });
        }
      );
    }
  )
);

/// ///////////////
// Access Token //
/// ///////////////

/**
 * Verifies that the passed in client ID and secret are correct
 *
 * @param  {String}     clientId            The ID of the client to check
 * @param  {String}     clientSecret        The secret to check
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {Client}     callback.client     The authenticated client
 * @api private
 */
const verifyClientAuthentication = function (clientId, clientSecret, callback) {
  OAuthDAO.Clients.getClientById(clientId, (error, client) => {
    if (error) {
      return callback(error);
    }

    if (!client) {
      return callback(null, false);
    }

    if (client.secret !== clientSecret) {
      log().warn({ client: client.id }, 'A client attempted to authenticate with the wrong secret');
      return callback(null, false);
    }

    return callback(null, client);
  });
};

/*!
 * BasicStrategy & ClientPasswordStrategy
 *
 * These strategies are used to authenticate registered OAuth clients.  They are
 * employed to protect the `token` endpoint, which consumers use to obtain
 * access tokens.  The OAuth 2.0 specification suggests that clients use the
 * HTTP Basic scheme to authenticate.  Use of the client password strategy
 * allows clients to send the same credentials in the request body (as opposed
 * to the `Authorization` header).  While this approach is not recommended by
 * the specification, in practice it is quite common.
 */
passport.use(new BasicStrategy(verifyClientAuthentication));
passport.use(new ClientPasswordStrategy(verifyClientAuthentication));

/**
 * @REST postAuthOauthV2Token
 *
 * Obtain an OAuth access token
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /auth/oauth/v2/token
 * @FormParam   {string}        grant_type      The authorization request type     [client_credentials]
 * @FormParam   {string}        client_id       The id of the OAuth client
 * @FormParam   {string}        client_secret   The secret of the OAuth client
 * @Return      {void}
 * @HttpResponse                200             token available
 * @HttpResponse                401             Unauthorized
 */
OAE.tenantRouter.on('post', '/api/auth/oauth/v2/token', [
  // OAuth allows for 2 possible strategies to authenticate an "Access Token Request" HTTP request
  // Currently we only support the "Client Credentials Grant".
  passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),

  // If authentication was succesful, hand out a token
  server.token(),

  // Take care of any errors that were triggered by the token handler
  server.errorHandler()
]);

/*
 * At this point, the client will not be authenticated yet, so we need
 * to add the CSRF exception
 */
OaeServer.addSafePathPrefix('/api/auth/oauth/v2/token');

/// ///////////////////
// Client endpoints //
/// ///////////////////

/**
 * @REST postAuthOauthClientsUserId
 *
 * Create an OAuth client
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /auth/oauth/clients/{userId}
 * @PathParam   {string}            userId          The id of the user for which to create an OAuth client
 * @FormParam   {string}            displayName     The name of the OAuth client
 * @Return      {OAuthClient}                       The created OAuth client
 * @HttpResponse                    201             client created
 * @HttpResponse                    400             Invalid user id was specified
 * @HttpResponse                    400             Missing or invalid displayname was provided
 * @HttpResponse                    401             Unauthorized
 */
OAE.tenantRouter.on('post', '/api/auth/oauth/clients/:userId', (request, response) => {
  OAuthAPI.Clients.createClient(
    request.ctx,
    request.params.userId,
    request.body.displayName,
    (error, client) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(201).send(client);
    }
  );
});

/**
 * @REST getAuthOauthClientsUserId
 *
 * Get all OAuth clients for a user
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /auth/oauth/clients/{userId}
 * @PathParam   {string}            userId          The id of the user for which to get the registered OAuth clients
 * @Return      {OAuthClientList}                   The registerd OAuth clients for the user
 * @HttpResponse                    200             clients available
 * @HttpResponse                    401             Unauthorized
 */
OAE.tenantRouter.on('get', '/api/auth/oauth/clients/:userId', (request, response) => {
  OAuthAPI.Clients.getClients(request.ctx, request.params.userId, (error, clients) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send({ results: clients });
  });
});

/**
 * @REST postAuthOauthClientsUserIdClientId
 *
 * Update an OAuth client
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /auth/oauth/clients/{userId}/{clientId}
 * @PathParam   {string}            userId          The id of the user to which the OAuth client is associated
 * @PathParam   {string}            clientId        The id of the OAuth client to update
 * @FormParam   {string}            displayName     The updated name for the OAuth client
 * @FormParam   {string}            secret          The updated secret for the OAuth client
 * @Return      {OAuthClient}                       The updated OAuth client
 * @HttpResponse                    200             client updated
 * @HttpResponse                    401             Unauthorized
 */
OAE.tenantRouter.on('post', '/api/auth/oauth/clients/:userId/:clientId', (request, response) => {
  OAuthAPI.Clients.updateClient(
    request.ctx,
    request.params.clientId,
    request.body.displayName,
    request.body.secret,
    (error, client) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(200).send(client);
    }
  );
});

/**
 * @REST deleteAuthOauthClientsUserIdClientId
 *
 * Delete an OAuth client
 *
 * @Server      tenant
 * @Method      DELETE
 * @Path        /auth/oauth/clients/{userId}/{clientId}
 * @PathParam   {string}            userId          The id of the user to which the OAuth client is associated
 * @PathParam   {string}            clientId        The id of the OAuth client to delete
 * @Return      {void}
 * @HttpResponse                    200             client deleted
 * @HttpResponse                    401             Unauthorized
 */
OAE.tenantRouter.on('delete', '/api/auth/oauth/clients/:userId/:clientId', (request, response) => {
  OAuthAPI.Clients.deleteClient(request.ctx, request.params.clientId, (error) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.sendStatus(200);
  });
});

export { OAE as default };