packages/oae-authentication/lib/strategies/oauth/rest.js
/*!
* 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 };