forked from redwoodjs/redwood
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request redwoodjs#1311 from jeliasson/feat/auth-azure-ad
Support Azure AD authentication: Implementation
- Loading branch information
Showing
8 changed files
with
178 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import jwt from 'jsonwebtoken' | ||
import jwksClient from 'jwks-rsa' | ||
|
||
const verifyAzureActiveDirectoryToken = ( | ||
bearerToken: string | ||
): Promise<null | Record<string, unknown>> => { | ||
return new Promise((resolve, reject) => { | ||
const { AZURE_ACTIVE_DIRECTORY_AUTHORITY } = process.env | ||
if (!AZURE_ACTIVE_DIRECTORY_AUTHORITY) { | ||
throw new Error( | ||
'`AZURE_ACTIVE_DIRECTORY_AUTHORITY` env var is not set.' | ||
) | ||
} | ||
|
||
/** @docs: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#sample-response */ | ||
const client = jwksClient({ | ||
jwksUri: `${AZURE_ACTIVE_DIRECTORY_AUTHORITY}/discovery/v2.0/keys`, | ||
}) | ||
|
||
jwt.verify( | ||
bearerToken, | ||
(header, callback) => { | ||
client.getSigningKey(header.kid as string, (error, key) => { | ||
callback(error, key.getPublicKey()) | ||
}) | ||
}, | ||
{ | ||
issuer: `${AZURE_ACTIVE_DIRECTORY_AUTHORITY}/v2.0`, | ||
algorithms: ['RS256'], | ||
}, | ||
(verifyError, decoded) => { | ||
if (verifyError) { | ||
return reject(verifyError) | ||
} | ||
resolve( | ||
typeof decoded === 'undefined' | ||
? null | ||
: (decoded as Record<string, unknown>) | ||
) | ||
} | ||
) | ||
}) | ||
} | ||
|
||
export const azureActiveDirectory = async ( | ||
token: string | ||
): Promise<null | Record<string, unknown>> => { | ||
return verifyAzureActiveDirectoryToken(token) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { UserAgentApplication as AzureActiveDirectory } from 'msal' | ||
|
||
export type { AzureActiveDirectory } | ||
import type { AuthClient } from './' | ||
|
||
export type AzureActiveDirectoryClient = AzureActiveDirectory | ||
export interface AzureActiveDirectoryUser {} | ||
|
||
export const azureActiveDirectory = (client: AzureActiveDirectoryClient): AuthClient => { | ||
return { | ||
type: 'azureActiveDirectory', | ||
client, | ||
login: async (options?) => await client.loginPopup(options), | ||
logout: (options?) => client.logout(options), | ||
signup: async (options?) => await client.loginPopup(options), | ||
getToken: async () => sessionStorage.getItem('msal.idtoken'), | ||
getUserMetadata: async () => (await client.getAccount()) || null, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
packages/cli/src/commands/generate/auth/providers/azureActiveDirectory.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// the lines that need to be added to index.js | ||
export const config = { | ||
imports: [`import { UserAgentApplication } from 'msal'`], | ||
init: `const azureActiveDirectoryClient = new UserAgentApplication({ | ||
auth: { | ||
clientId: process.env.AZURE_ACTIVE_DIRECTORY_CLIENT_ID, | ||
authority: process.env.AZURE_ACTIVE_DIRECTORY_AUTHORITY, | ||
redirectUri: process.env.AZURE_ACTIVE_DIRECTORY_REDIRECT_URI, | ||
postLogoutRedirectUri: process.env.AZURE_ACTIVE_DIRECTORY_LOGOUT_REDIRECT_URI, | ||
}, | ||
})`, | ||
authProvider: { | ||
client: 'azureActiveDirectoryClient', | ||
type: 'azureActiveDirectory', | ||
}, | ||
} | ||
|
||
// required packages to install | ||
export const webPackages = ['msal'] | ||
export const apiPackages = [] | ||
|
||
// any notes to print out when the job is done | ||
export const notes = [ | ||
'You will need to create several environment variables with your Azure AD config options. Check out web/src/index.js for the variables you need to add.', | ||
'\n', | ||
'RedwoodJS specific Documentation:', | ||
'https://redwoodjs.com/docs/authentication#azure-ad', | ||
'\n', | ||
'MSAL.js Documentation:', | ||
'https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-js-initializing-client-applications' | ||
] |
61 changes: 61 additions & 0 deletions
61
packages/cli/src/commands/generate/auth/templates/azureActiveDirectory.auth.js.template
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// Define what you want `currentUser` to return throughout your app. For example, | ||
// to return a real user from your database, you could do something like: | ||
// | ||
// export const getCurrentUser = async ({ email }) => { | ||
// return await db.user.findOne({ where: { email } }) | ||
// } | ||
|
||
import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api' | ||
|
||
export const getCurrentUser = async (decoded, { token, type }) => { | ||
return { | ||
email: decoded.preferred_username ?? null, | ||
...decoded, | ||
roles: parseJWT({ decoded }).roles | ||
} | ||
} | ||
|
||
// Use this function in your services to check that a user is logged in, and | ||
// optionally raise an error if they're not. | ||
|
||
/** | ||
* Use requireAuth in your services to check that a user is logged in, | ||
* whether or not they are assigned a role, and optionally raise an | ||
* error if they're not. | ||
* | ||
* @param {string=} roles - An optional role or list of roles | ||
* @param {string[]=} roles - An optional list of roles | ||
|
||
* @example | ||
* | ||
* // checks if currentUser is authenticated | ||
* requireAuth() | ||
* | ||
* @example | ||
* | ||
* // checks if currentUser is authenticated and assigned one of the given roles | ||
* requireAuth({ role: 'admin' }) | ||
* requireAuth({ role: ['editor', 'author'] }) | ||
* requireAuth({ role: ['publisher'] }) | ||
*/ | ||
export const requireAuth = ({ role } = {}) => { | ||
if (!context.currentUser) { | ||
throw new AuthenticationError("You don't have permission to do that.") | ||
} | ||
|
||
if ( | ||
typeof role !== 'undefined' && | ||
typeof role === 'string' && | ||
!context.currentUser.roles?.includes(role) | ||
) { | ||
throw new ForbiddenError("You don't have access to do that.") | ||
} | ||
|
||
if ( | ||
typeof role !== 'undefined' && | ||
Array.isArray(role) && | ||
!context.currentUser.roles?.some((r) => role.includes(r)) | ||
) { | ||
throw new ForbiddenError("You don't have access to do that.") | ||
} | ||
} |