Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getCurrentUser given both decoded & access tokens #779

Merged
merged 22 commits into from
Jul 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
042cd8e
getCurrentUser given both decoded & access tokens
dthyresson Jun 30, 2020
5b6a638
Corrects default case in decodeAuthToken
dthyresson Jun 30, 2020
8d71532
Adds params to generated auth.js getCurrentUser
dthyresson Jun 30, 2020
2affd4d
Refactor w/ AuthDecoder interface to decode token
dthyresson Jul 3, 2020
e337806
Netlify & GoTrue decoders missing NewClientContext
dthyresson Jul 3, 2020
5a98b7d
Changes requested for AuthorizationHeader plus ++
dthyresson Jul 3, 2020
171852b
Implemented getAuthenticationContext
dthyresson Jul 6, 2020
0b6acac
Get to green w/ fixed getAuthenticationContext
dthyresson Jul 6, 2020
64b48b9
Clean up getAuthenticationContext fall through
dthyresson Jul 6, 2020
e7ac26c
Implemented code style changes per PR review.
dthyresson Jul 7, 2020
e88cc88
Updates cli generate auth for new currentUser
dthyresson Jul 7, 2020
13f21de
Restores redirect_uri on auth0 provider generator
dthyresson Jul 8, 2020
bb1e5b5
Refactor auth implementation.
peterp Jul 9, 2020
2be2b47
Added warning about auth0 localstorage.
peterp Jul 9, 2020
ff23e33
Resolve graphql path.
peterp Jul 9, 2020
14bb24f
Reorder auth generator steps.
peterp Jul 9, 2020
bc56397
Do not install auth0 package by default.
peterp Jul 9, 2020
7934823
Revert template modifications.
peterp Jul 9, 2020
b078051
Fix AuthContextPayload.
peterp Jul 9, 2020
af4827c
Update docs.
peterp Jul 9, 2020
2caf290
Update packages/cli/src/commands/generate/auth/templates/firebase.aut…
peterp Jul 9, 2020
c519405
Merge branch 'main' into dt-auth-access-token
peterp Jul 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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