Skip to content

Commit

Permalink
Merge pull request #1383 from sharetribe/oidc-proxy
Browse files Browse the repository at this point in the history
OIDC proxy implementation
  • Loading branch information
OtterleyW authored Dec 16, 2020
2 parents 8aaf42a + 777ad3a commit e2893bd
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern:

## Upcoming version 2020-XX-XX

- [add] Add helper functions for setting up your own OIDC authentication and using FTW server as
proxy when needed. [#1383](https://github.com/sharetribe/ftw-daily/pull/1383)

## [v7.1.0] 2020-12-15

- [change] Handle entity update with sparse attributes.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"full-icu": "^1.3.1",
"helmet": "^4.0.0",
"intl-pluralrules": "^1.0.3",
"jose": "3.1.0",
"lodash": "^4.17.19",
"mapbox-gl-multitouch": "^1.0.3",
"moment": "^2.22.2",
Expand Down
100 changes: 100 additions & 0 deletions server/api-util/idToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const crypto = require('crypto');
const { default: fromKeyLike } = require('jose/jwk/from_key_like');
const { default: SignJWT } = require('jose/jwt/sign');

const radix = 10;
const PORT = parseInt(process.env.REACT_APP_DEV_API_SERVER_PORT, radix);
const rootUrl = process.env.REACT_APP_CANONICAL_ROOT_URL;
const useDevApiServer = process.env.NODE_ENV === 'development' && !!PORT;

const issuerUrl = useDevApiServer ? `http://localhost:${PORT}/api` : `${rootUrl}/api`;

/**
* Gets user information and creates the signed jwt for id token.
*
* @param {string} idpClientId the client id of the idp provider in Console
* @param {Object} options signing options containing signingAlg and required key information
* @param {Object} user user information containing at least firstName, lastName, email and emailVerified
*
* @return {Promise} idToken
*/
exports.createIdToken = (idpClientId, user, options) => {
if (!idpClientId) {
console.error('Missing idp client id!');
return;
}
if (!user) {
console.error('Missing user information!');
return;
}

const signingAlg = options.signingAlg;

// Currently Flex supports only RS256 signing algorithm.
if (signingAlg !== 'RS256') {
console.error(`${signingAlg} is not currently supported!`);
return;
}

const { rsaPrivateKey, keyId } = options;

if (!rsaPrivateKey) {
console.error('Missing RSA private key!');
return;
}

// We use jose library which requires the RSA key
// to be KeyLike format:
// https://github.com/panva/jose/blob/master/docs/types/_types_d_.keylike.md
const privateKey = crypto.createPrivateKey(rsaPrivateKey);

const { userId, firstName, lastName, email, emailVerified } = user;

const jwt = new SignJWT({
given_name: firstName,
family_name: lastName,
email: email,
email_verified: emailVerified,
})
.setProtectedHeader({ alg: signingAlg, kid: keyId })
.setIssuedAt()
.setIssuer(issuerUrl)
.setSubject(userId)
.setAudience(idpClientId)
.setExpirationTime('1h')
.sign(privateKey);

return jwt;
};

// Serves the discovery document in json format
// this document is expected to be found from
// api/.well-known/openid-configuration endpoint
exports.openIdConfiguration = (req, res) => {
res.json({
issuer: issuerUrl,
jwks_uri: `${issuerUrl}/.well-known/jwks.json`,
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
});
};

/**
* @param {String} signingAlg signing algorithm, currently only RS256 is supported
* @param {Array} list containing keys to be served in json endpoint
*
* // Serves the RSA public key as JWK
// this document is expected to be found from
// api/.well-known/jwks.json endpoint as stated in discovery document
*/
exports.jwksUri = keys => (req, res) => {
const jwkKeys = keys.map(key => {
return fromKeyLike(crypto.createPublicKey(key.rsaPublicKey)).then(res => {
return { alg: key.alg, kid: key.keyId, ...res };
});
});

Promise.all(jwkKeys).then(resolvedJwkKeys => {
res.json({ keys: resolvedJwkKeys });
});
};
4 changes: 2 additions & 2 deletions server/api/auth/loginWithIdp.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const httpsAgent = new https.Agent({ keepAlive: true });

const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};

module.exports = (err, user, req, res, clientID, idpId) => {
module.exports = (err, user, req, res, idpClientId, idpId) => {
if (err) {
log.error(err, 'fetching-user-data-from-idp-failed');

Expand Down Expand Up @@ -87,7 +87,7 @@ module.exports = (err, user, req, res, clientID, idpId) => {
return sdk
.loginWithIdp({
idpId,
idpClientId: clientID,
idpClientId,
idpToken: user.idpToken,
})
.then(response => {
Expand Down
18 changes: 18 additions & 0 deletions server/apiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const createUserWithIdp = require('./api/auth/createUserWithIdp');
const { authenticateFacebook, authenticateFacebookCallback } = require('./api/auth/facebook');
const { authenticateGoogle, authenticateGoogleCallback } = require('./api/auth/google');

const { openIdConfiguration, jwksUri } = require('./api-util/idToken');

const router = express.Router();

// ================ API router middleware: ================ //
Expand Down Expand Up @@ -80,4 +82,20 @@ router.get('/auth/google', authenticateGoogle);
// loginWithIdp endpoint in Flex API to authenticate user to Flex
router.get('/auth/google/callback', authenticateGoogleCallback);

// These endpoints will be used if you FTW as OIDC proxy
// https://www.sharetribe.com/docs/cookbook-social-logins-and-sso/setup-open-id-connect-proxy/
// All identity providers should provide an OpenID Connect discovery document:
// https://openid.net/specs/openid-connect-discovery-1_0.html
// And in the discovery document we need to define jwks_uri attribute
// which denotes the location of public signing keys

const rsaSecretKey = process.env.RSA_SECRET_KEY;
const rsaPublicKey = process.env.RSA_PUBLIC_KEY;
const keyId = process.env.KEY_ID;

if (rsaPublicKey && rsaSecretKey) {
router.get('/.well-known/openid-configuration', openIdConfiguration);
router.get('/.well-known/jwks.json', jwksUri([{ alg: 'RS256', rsaPublicKey, keyId }]));
}

module.exports = router;
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7634,6 +7634,11 @@ [email protected]:
import-local "^3.0.2"
jest-cli "^26.6.0"

[email protected]:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jose/-/jose-3.1.0.tgz#31a48b76a2e0f5da4e9a1be261e430e0bfaa4a43"
integrity sha512-TLZFF0qAPlG0GZDrPw9HAiWKJcDuUbOp1WdjuS5cJ0reTzd1zS718zrUPOt7BIOViTA6PZpEnMt5cMQttJq3QA==

js-cookie@^2.1.3:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
Expand Down

0 comments on commit e2893bd

Please sign in to comment.