Skip to content

Commit

Permalink
Merge pull request #779 from dthyresson/dt-auth-access-token
Browse files Browse the repository at this point in the history
getCurrentUser given both decoded & access tokens
  • Loading branch information
peterp committed Jul 9, 2020
2 parents 6b8eaed + c519405 commit 7b0fb8f
Show file tree
Hide file tree
Showing 18 changed files with 213 additions and 166 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
"statusBar.foreground": "#e7e7e7",
"statusBar.border": "#b85833"
},
"peacock.color": "#b85833"
"peacock.color": "#b85833",
"typescript.tsdk": "node_modules/typescript/lib"
}
98 changes: 0 additions & 98 deletions packages/api/src/auth/authHeaders.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'

// https://auth0.com/docs/api-auth/tutorials/verify-access-token
/**
* This takes an auth0 jwt and verifies it. It returns something like this:
* ```js
Expand All @@ -16,7 +15,7 @@ import jwksClient from 'jwks-rsa'
* }
* ```
*
* You can use `sub` as a stable reference to your user, buti f you want the email
* You can use `sub` as a stable reference to your user, but if you want the email
* addres you can set a context object[^0] in rules[^1]:
*
* ^0: https://auth0.com/docs/rules/references/context-object
Expand Down Expand Up @@ -59,3 +58,7 @@ export const verifyAuth0Token = (
)
})
}

export const auth0 = async (token: string): Promise<null | object> => {
return verifyAuth0Token(token)
}
35 changes: 35 additions & 0 deletions packages/api/src/auth/decoders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { GlobalContext } from 'src/globalContext'
import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'
import type { SupportedAuthTypes } from '@redwoodjs/auth'

import { netlify } from './netlify'
import { auth0 } from './auth0'
const noop = (token: string) => token

const typesToDecoders: Record<SupportedAuthTypes, Function> = {
auth0: auth0,
netlify: netlify,
goTrue: netlify,
magicLink: noop,
firebase: noop,
custom: noop,
}

export const decodeToken = async (
type: SupportedAuthTypes,
token: string,
req: {
event: APIGatewayProxyEvent
context: GlobalContext & LambdaContext
}
): Promise<null | string | object> => {
if (!typesToDecoders[type]) {
throw new Error(
`The auth type "${type}" is not supported, we currently support: ${Object.keys(
typesToDecoders
).join(', ')}`
)
}
const decoder = typesToDecoders[type]
return decoder(token, req)
}
19 changes: 19 additions & 0 deletions packages/api/src/auth/decoders/netlify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Context as LambdaContext, ClientContext } from 'aws-lambda'
import jwt from 'jsonwebtoken'

type NetlifyContext = ClientContext & {
user?: object
}

export const netlify = (token: string, req: { context: LambdaContext }) => {
// Netlify verifies and decodes the JWT before the request is passed to our Serverless
// function, so the decoded JWT is already available in production.
if (process.env.NODE_ENV === 'production') {
const clientContext = req.context.clientContext as NetlifyContext
return clientContext?.user || null
} else {
// We emulate the native Netlify experience in development mode.
// We just decode it since we don't have the signing key.
return jwt.decode(token)
}
}
61 changes: 61 additions & 0 deletions packages/api/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { GlobalContext } from 'src/globalContext'
import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'
import type { SupportedAuthTypes } from '@redwoodjs/auth'

import { decodeToken } from './decoders'

// This is shared by `@redwoodjs/web`
const AUTH_PROVIDER_HEADER = 'auth-provider'

export const getAuthProviderHeader = (
event: APIGatewayProxyEvent
): SupportedAuthTypes => {
return event?.headers[AUTH_PROVIDER_HEADER] as SupportedAuthTypes
}

export interface AuthorizationHeader {
schema: 'Bearer' | 'Basic'
token: string
}
/**
* Split the `Authorization` header into a schema and token part.
*/
export const parseAuthorizationHeader = (
event: APIGatewayProxyEvent
): AuthorizationHeader => {
const [schema, token] = event.headers?.authorization?.split(' ')
if (!schema.length || !token.length) {
throw new Error('The `Authorization` header is not valid.')
}
// @ts-expect-error
return { schema, token }
}

export type AuthContextPayload = [
string | object | null,
{ type: SupportedAuthTypes; token: string }
]

/**
* Get the authorization information from the request headers and request context.
* @returns [decoded, { type, token }]
**/
export const getAuthenticationContext = async ({
event,
context,
}: {
event: APIGatewayProxyEvent
context: GlobalContext & LambdaContext
}): Promise<undefined | AuthContextPayload> => {
const type = getAuthProviderHeader(event)
// No `auth-provider` header means that the user is logged out,
// and none of this is auth malarky is required.
if (!type) {
return undefined
}

let decoded = null
const { token } = parseAuthorizationHeader(event)
decoded = await decodeToken(type, token, { event, context })
return [decoded, { type, token }]
}
49 changes: 22 additions & 27 deletions packages/api/src/functions/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'
import type { Config } from 'apollo-server-lambda'
import type { Context, ContextFunction } from 'apollo-server-core'
import type { AuthToken } from 'src/auth/authHeaders'
import type { GlobalContext } from 'src/globalContext'
//
import type { AuthContextPayload } from 'src/auth'
import { ApolloServer } from 'apollo-server-lambda'
import { getAuthProviderType, decodeAuthToken } from 'src/auth/authHeaders'
import { getAuthenticationContext } from 'src/auth'
import { setContext } from 'src/globalContext'

export type GetCurrentUser = (
token: AuthToken
decoded: AuthContextPayload[0],
raw: AuthContextPayload[1]
) => Promise<null | object | string>

/**
* We use Apollo Server's `context` option as an entry point for constructing our own
* global context object.
* We use Apollo Server's `context` option as an entry point to construct our
* own global context.
*
* Context explained by Apollo's Docs:
* Context is an object shared by all resolvers in a particular query,
Expand All @@ -33,34 +33,30 @@ export const createContextHandler = (
event: APIGatewayProxyEvent
context: GlobalContext & LambdaContext
}) => {
// Prevent the Lambda function from waiting for all resources,
// such as database connections, to be released before returning a reponse.
// Prevent the Serverless function from waiting for all resources (db connections)
// to be released before returning a reponse.
context.callbackWaitsForEmptyEventLoop = false

// Get the authorization information from the request headers and request context.
const type = getAuthProviderType(event)
if (typeof type !== 'undefined') {
const authToken = await decodeAuthToken({ type, event, context })
context.currentUser =
typeof getCurrentUser == 'function'
? await getCurrentUser(authToken)
: authToken
// If the request contains authorization headers, we'll decode the providers that we support,
// and pass those to the `currentUser`.
const authContext = await getAuthenticationContext({ event, context })
if (authContext) {
context.currentUser = getCurrentUser
? getCurrentUser(authContext[0], authContext[1])
: authContext
}

// Sets the **global** context object, which can be imported with:
// import { context } from '@redwoodjs/api'
let customUserContext = userContext
if (typeof userContext === 'function') {
// if userContext is a function, run that and return just the result
const userContextData = await userContext({ event, context })
return setContext({
...context,
...userContextData,
})
customUserContext = await userContext({ event, context })
}

// Sets the **global** context object, which can be imported with:
// import { context } from '@redwoodjs/api'
return setContext({
...context,
...userContext,
...customUserContext,
})
}
}
Expand All @@ -76,8 +72,7 @@ interface GraphQLHandlerOptions extends Config {
*/
getCurrentUser?: GetCurrentUser
/**
* A callback when an unhandled exception occurs. Use this to disconnect your prisma
* instance.
* A callback when an unhandled exception occurs. Use this to disconnect your prisma instance.
*/
onException?: () => void
}
Expand Down Expand Up @@ -110,7 +105,7 @@ export const createGraphQLHandler = (
if (isDevEnv) {
// I want the dev-server to pick this up!?
// TODO: Move the error handling into a separate package
// @ts-ignore
// @ts-expect-error
import('@redwoodjs/dev-server/dist/error')
.then(({ handleError }) => {
return handleError(error.originalError as Error)
Expand Down
1 change: 0 additions & 1 deletion packages/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ export * from './makeServices'
export * from './makeMergedSchema/makeMergedSchema'
export * from './functions/graphql'
export * from './globalContext'
export * from './auth/authHeaders'
3 changes: 2 additions & 1 deletion packages/api/src/makeMergedSchema/rootSchema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { GlobalContext } from 'src/globalContext'
import gql from 'graphql-tag'
import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json'
import type { GlobalContext } from 'src/globalContext'


// @ts-ignore - not inside the <rootDir>
import apiPackageJson from 'src/../package.json'
Expand Down
10 changes: 5 additions & 5 deletions packages/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ Our recommendation is to create a `src/lib/auth.js|ts` file that exports a `getC
```js
import { getCurrentUser } from 'src/lib/auth'
// Example:
// export const getCurrentUser = async (authToken: { email }) => {
// return await db.user.findOne({ where: { email } })
// export const getCurrentUser = async (decoded) => {
// return await db.user.findOne({ where: { decoded.email } })
// }
``

Expand Down Expand Up @@ -306,13 +306,13 @@ Magic.link recommends using the issuer as the userID.
// redwood/api/src/lib/auth.ts
import { Magic } from '@magic-sdk/admin'

export const getCurrentUser = async (authToken) => {
export const getCurrentUser = async (_decoded, { token }) => {
const mAdmin = new Magic(process.env.MAGICLINK_SECRET)
const {
email,
publicAddress,
issuer,
} = await mAdmin.users.getMetadataByToken(authToken)
} = await mAdmin.users.getMetadataByToken(token)

return await db.user.findOne({ where: { issuer } })
}
Expand Down Expand Up @@ -381,5 +381,5 @@ You'll need to import the type definition for you client and add it to the suppo

```ts
// authClients/index.ts
export type SupportedAuthClients = Auth0 | GoTrue | NetlifyIdentity | MagicLinks
export type SupportedAuthClients = Auth0 | GoTrue | NetlifyIdentity | MagicLink
```
Loading

0 comments on commit 7b0fb8f

Please sign in to comment.