Skip to content

Commit

Permalink
Merge pull request redwoodjs#1311 from jeliasson/feat/auth-azure-ad
Browse files Browse the repository at this point in the history
Support Azure AD authentication: Implementation
  • Loading branch information
Tobbe authored Dec 3, 2020
2 parents 61c75de + 086bd3e commit 2986015
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 2 deletions.
49 changes: 49 additions & 0 deletions packages/api/src/auth/decoders/azureActiveDirectory.ts
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)
}
2 changes: 2 additions & 0 deletions packages/api/src/auth/decoders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SupportedAuthTypes } from '@redwoodjs/auth'
import type { GlobalContext } from 'src/globalContext'

import { auth0 } from './auth0'
import { azureActiveDirectory } from './azureActiveDirectory'
import { netlify } from './netlify'
import { supabase } from './supabase'
const noop = (token: string) => token
Expand All @@ -22,6 +23,7 @@ const typesToDecoders: Record<
| ((token: string, req: Req) => Decoded | Promise<Decoded>)
> = {
auth0: auth0,
azureActiveDirectory: azureActiveDirectory,
netlify: netlify,
goTrue: netlify,
magicLink: noop,
Expand Down
10 changes: 9 additions & 1 deletion packages/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,13 @@ If you want to use the useAuth hook `Sign Up` with Auth0 to default the UI to th

```ts
// authClients/index.ts
export type SupportedAuthClients = Auth0 | GoTrue | NetlifyIdentity | MagicLink
export type SupportedAuthClients =
| Auth0
| AzureActiveDirectory
| GoTrue
| NetlifyIdentity
| MagicLink
| Firebase
| Supabase
| Custom
```
1 change: 1 addition & 0 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"firebase-admin": "^9.1.1",
"gotrue-js": "git://github.com/netlify/gotrue-js.git#28df09cfcac505feadcb1719714d2a9507cf68eb",
"magic-sdk": "^2.5.0",
"msal": "^1.4.1",
"netlify-identity-widget": "1.9.1",
"react": "^16.13.1"
},
Expand Down
19 changes: 19 additions & 0 deletions packages/auth/src/authClients/azureActiveDirectory.ts
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,
}
}
7 changes: 6 additions & 1 deletion packages/auth/src/authClients/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Auth0, Auth0User } from './auth0'
import { auth0 } from './auth0'
import type { AzureActiveDirectory, AzureActiveDirectoryUser } from './azureActiveDirectory'
import { azureActiveDirectory } from './azureActiveDirectory'
import type { Custom } from './custom'
import { custom } from './custom'
import type { Firebase } from './firebase'
Expand All @@ -11,12 +13,12 @@ import { magicLink } from './magicLink'
import type { NetlifyIdentity } from './netlify'
import { netlify } from './netlify'
import type { Supabase, SupabaseUser } from './supabase'
//
import { supabase } from './supabase'

const typesToClients = {
netlify,
auth0,
azureActiveDirectory,
goTrue,
magicLink,
firebase,
Expand All @@ -27,6 +29,7 @@ const typesToClients = {

export type SupportedAuthClients =
| Auth0
| AzureActiveDirectory
| GoTrue
| NetlifyIdentity
| MagicLink
Expand All @@ -37,11 +40,13 @@ export type SupportedAuthClients =
export type SupportedAuthTypes = keyof typeof typesToClients

export type { Auth0User }
export type { AzureActiveDirectoryUser }
export type { GoTrueUser }
export type { MagicUser }
export type { SupabaseUser }
export type SupportedUserMetadata =
| Auth0User
| AzureActiveDirectoryUser
| GoTrueUser
| MagicUser
| SupabaseUser
Expand Down
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'
]
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.")
}
}

0 comments on commit 2986015

Please sign in to comment.