Skip to content

Commit

Permalink
Merge branch 'main' into tobbe-router-whileloadingauth-hook
Browse files Browse the repository at this point in the history
  • Loading branch information
jtoar authored Jun 26, 2022
2 parents f6e2b3f + 647065c commit 358dc83
Show file tree
Hide file tree
Showing 225 changed files with 37,648 additions and 2,632 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,37 @@ And there you have it.
</tr>
<tr>
<td align="center"><a href="https://fabiolazzaroni.dev/"><img src="https://avatars.githubusercontent.com/u/15056746?v=4" width="100px;" alt=""/><br /><sub><b>Fabio Lazzaroni</b></sub></a></td>
<td align="center"><a href="https://github.com/rushabhhere"><img src="https://avatars.githubusercontent.com/u/73743535?v=4" width="100px;" alt=""/><br /><sub><b>Rushabh Javeri</b></sub></a></td>
<td align="center"><a href="https://github.com/andershagbard"><img src="https://avatars.githubusercontent.com/u/9662430?v=4" width="100px;" alt=""/><br /><sub><b>Anders Søgaard</b></sub></a></td>
<td align="center"><a href="https://github.com/kunalarya"><img src="https://avatars.githubusercontent.com/u/1680103?v=4" width="100px;" alt=""/><br /><sub><b>kunalarya</b></sub></a></td>
<td align="center"><a href="https://github.com/alephao"><img src="https://avatars.githubusercontent.com/u/7674479?v=4" width="100px;" alt=""/><br /><sub><b>Aleph Retamal</b></sub></a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/AlonHor"><img src="https://avatars.githubusercontent.com/u/57628667?v=4" width="100px;" alt=""/><br /><sub><b>Alon</b></sub></a></td>
<td align="center"><a href="https://ionoid.io/"><img src="https://avatars.githubusercontent.com/u/1108370?v=4" width="100px;" alt=""/><br /><sub><b>Bouzid Badreddine</b></sub></a></td>
<td align="center"><a href="https://charlypoly.com/"><img src="https://avatars.githubusercontent.com/u/1252066?v=4" width="100px;" alt=""/><br /><sub><b>Charly POLY</b></sub></a></td>
<td align="center"><a href="https://github.com/MrGuiMan"><img src="https://avatars.githubusercontent.com/u/3082385?v=4" width="100px;" alt=""/><br /><sub><b>Guillaume Mantopoulos</b></sub></a></td>
<td align="center"><a href="https://github.com/jaaneh"><img src="https://avatars.githubusercontent.com/u/27323317?v=4" width="100px;" alt=""/><br /><sub><b>Jan Henning</b></sub></a></td>
</tr>
<tr>
<td align="center"><a href="http://oberschweiber.com/"><img src="https://avatars.githubusercontent.com/u/19388?v=4" width="100px;" alt=""/><br /><sub><b>Jonas Oberschweiber</b></sub></a></td>
<td align="center"><a href="https://jordanrolph.com/"><img src="https://avatars.githubusercontent.com/u/28222941?v=4" width="100px;" alt=""/><br /><sub><b>Jordan Rolph</b></sub></a></td>
<td align="center"><a href="https://github.com/jorgepvenegas"><img src="https://avatars.githubusercontent.com/u/2190603?v=4" width="100px;" alt=""/><br /><sub><b>Jorge Venegas</b></sub></a></td>
<td align="center"><a href="https://github.com/razzeee"><img src="https://avatars.githubusercontent.com/u/5943908?v=4" width="100px;" alt=""/><br /><sub><b>Kolja Lampe</b></sub></a></td>
<td align="center"><a href="https://github.com/Leon-Sam"><img src="https://avatars.githubusercontent.com/u/18523441?v=4" width="100px;" alt=""/><br /><sub><b>Leon</b></sub></a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Masvoras"><img src="https://avatars.githubusercontent.com/u/58081001?v=4" width="100px;" alt=""/><br /><sub><b>Masvoras</b></sub></a></td>
<td align="center"><a href="https://minho42.com/"><img src="https://avatars.githubusercontent.com/u/15278512?v=4" width="100px;" alt=""/><br /><sub><b>Min ho Kim</b></sub></a></td>
<td align="center"><a href="https://github.com/fangpinsern"><img src="https://avatars.githubusercontent.com/u/52379442?v=4" width="100px;" alt=""/><br /><sub><b>Pin Sern</b></sub></a></td>
<td align="center"><a href="https://click.ecc.ac.jp/ecc/rokazaki/"><img src="https://avatars.githubusercontent.com/u/70571576?v=4" width="100px;" alt=""/><br /><sub><b>RUI OKAZAKI</b></sub></a></td>
<td align="center"><a href="https://github.com/Gombeng"><img src="https://avatars.githubusercontent.com/u/57914770?v=4" width="100px;" alt=""/><br /><sub><b>Syahrizal Ardana</b></sub></a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/craineum"><img src="https://avatars.githubusercontent.com/u/2641685?v=4" width="100px;" alt=""/><br /><sub><b>craineum</b></sub></a></td>
<td align="center"><a href="https://github.com/gtarsia"><img src="https://avatars.githubusercontent.com/u/4072352?v=4" width="100px;" alt=""/><br /><sub><b>hello there</b></sub></a></td>
<td align="center"><a href="https://github.com/mattdriscoll"><img src="https://avatars.githubusercontent.com/u/16880374?v=4" width="100px;" alt=""/><br /><sub><b>Matt Driscoll</b></sub></a></td>
<td align="center"><a href="http://paikwiki.github.io/"><img src="https://avatars.githubusercontent.com/u/4120850?v=4" width="100px;" alt=""/><br /><sub><b>paikwiki</b></sub></a></td>
</tr>
</table>

Expand Down
4 changes: 2 additions & 2 deletions __fixtures__/test-project/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "1.5.1",
"@redwoodjs/graphql-server": "1.5.1"
"@redwoodjs/api": "2.0.0",
"@redwoodjs/graphql-server": "2.0.0"
}
}
8 changes: 4 additions & 4 deletions __fixtures__/test-project/api/src/functions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ export const handler = async (event, context) => {

const resetPasswordOptions = {
// handler() is invoked after the password has been successfully updated in
// the database. Returning anything truthy will automatically logs the user
// the database. Returning anything truthy will automatically log the user
// in. Return `false` otherwise, and in the Reset Password page redirect the
// user to the login page.
handler: (user) => {
return user
},

// If `false` then the new password MUST be different than the current one
// If `false` then the new password MUST be different from the current one
allowReusedPassword: true,

errors: {
Expand Down Expand Up @@ -125,7 +125,7 @@ export const handler = async (event, context) => {
db: db,

// The name of the property you'd call on `db` to access your user table.
// ie. if your Prisma model is named `User` this value would be `user`, as in `db.user`
// i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
authModelAccessor: 'user',

// A map of what dbAuth calls a field to what your database calls it.
Expand All @@ -146,7 +146,7 @@ export const handler = async (event, context) => {
HttpOnly: true,
Path: '/',
SameSite: 'Strict',
Secure: process.env.NODE_ENV !== 'development' ? true : false,
Secure: process.env.NODE_ENV !== 'development',

// If you need to allow other domains (besides the api side) access to
// the dbAuth session cookie:
Expand Down
2 changes: 1 addition & 1 deletion __fixtures__/test-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
]
},
"devDependencies": {
"@redwoodjs/core": "1.5.1"
"@redwoodjs/core": "2.0.0"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config",
Expand Down
8 changes: 4 additions & 4 deletions __fixtures__/test-project/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
]
},
"dependencies": {
"@redwoodjs/auth": "1.5.1",
"@redwoodjs/forms": "1.5.1",
"@redwoodjs/router": "1.5.1",
"@redwoodjs/web": "1.5.1",
"@redwoodjs/auth": "2.0.0",
"@redwoodjs/forms": "2.0.0",
"@redwoodjs/router": "2.0.0",
"@redwoodjs/web": "2.0.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2"
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/a11y.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,5 @@ export default ContactPage
`RouteFocus` tells the router to send focus to it's child on page change. In the example above, when the user navigates to the contact page, the name text field on the form is focused—the first field of the form they're here to fill out.

<div class="video-container">
<iframe src="https://www.youtube.com/embed/T1zs77LU68w?t=3240" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; modestbranding; showinfo=0" allowfullscreen></iframe>
<iframe src="https://www.youtube.com/embed/T1zs77LU68w?t=3240" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; modestbranding; showinfo=0; fullscreen"></iframe>
</div>
264 changes: 264 additions & 0 deletions docs/docs/auth/auth0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
---
sidebar_label: Auth0
---

# Auth0 Authentication

The following CLI command will install required packages and generate boilerplate code and files for Redwood Projects:

```bash
yarn rw setup auth auth0
```

_If you prefer to manually install the package and add code_, run the following command and then add the required code provided in the next section.

```bash
cd web
yarn add @redwoodjs/auth @auth0/auth0-spa-js
```

## Setup

To get your application keys, only complete the ["Configure Auth0"](https://auth0.com/docs/quickstart/spa/react#get-your-application-keys) section of the SPA Quickstart guide.

**NOTE** If you're using Auth0 with Redwood then you must also [create an API](https://auth0.com/docs/quickstart/spa/react/02-calling-an-api#create-an-api) and set the audience parameter, or you'll receive an opaque token instead of the required JWT token.

The `useRefreshTokens` options is required for automatically extending sessions beyond that set in the initial JWT expiration (often 3600/1 hour or 86400/1 day).

If you want to allow users to get refresh tokens while offline, you must also enable the Allow Offline Access switch in your Auth0 API Settings as part of setup configuration. See: [https://auth0.com/docs/tokens/refresh-tokens](https://auth0.com/docs/tokens/refresh-tokens)

You can increase security by using refresh token rotation which issues a new refresh token and invalidates the predecessor token with each request made to Auth0 for a new access token.

Rotating the refresh token reduces the risk of a compromised refresh token. For more information, see: [https://auth0.com/docs/tokens/refresh-tokens/refresh-token-rotation](https://auth0.com/docs/tokens/refresh-tokens/refresh-token-rotation).

> **Including Environment Variables in Serverless Deployment:** in addition to adding the following env vars to your deployment hosting provider, you _must_ take an additional step to include them in your deployment build process. Using the names exactly as given below, follow the instructions in [this document](environment-variables.md) to include them in your `redwood.toml`.
```jsx title="web/src/App.js"
import { AuthProvider } from '@redwoodjs/auth'
import { Auth0Client } from '@auth0/auth0-spa-js'
import { FatalErrorBoundary } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'

import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'

import './index.css'

const auth0 = new Auth0Client({
domain: process.env.AUTH0_DOMAIN,
client_id: process.env.AUTH0_CLIENT_ID,
redirect_uri: process.env.AUTH0_REDIRECT_URI,

// ** NOTE ** Storing tokens in browser local storage provides persistence across page refreshes and browser tabs.
// However, if an attacker can achieve running JavaScript in the SPA using a cross-site scripting (XSS) attack,
// they can retrieve the tokens stored in local storage.
// https://auth0.com/docs/libraries/auth0-spa-js#change-storage-options
cacheLocation: 'localstorage',
audience: process.env.AUTH0_AUDIENCE,

// @MARK: useRefreshTokens is required for automatically extending sessions
// beyond that set in the initial JWT expiration.
//
// @MARK: https://auth0.com/docs/tokens/refresh-tokens
// useRefreshTokens: true,
})

const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<AuthProvider client={auth0} type="auth0">
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</FatalErrorBoundary>
)

export default App
```

## Login and Logout Options

When using the Auth0 client, `login` and `logout` take `options` that can be used to override the client config:

- `returnTo`: a permitted logout url set in Auth0
- `redirectTo`: a target url after login

The latter is helpful when an unauthenticated user visits a Private route, but then is redirected to the `unauthenticated` route. The Redwood router will place the previous requested path in the pathname as a `redirectTo` parameter which can be extracted and set in the Auth0 `appState`. That way, after successfully logging in, the user will be directed to this `targetUrl` rather than the config's callback.

```jsx
const UserAuthTools = () => {
const { loading, isAuthenticated, logIn, logOut } = useAuth()

if (loading) {
// auth is rehydrating
return null
}

return (
<Button
onClick={async () => {
if (isAuthenticated) {
await logOut({ returnTo: process.env.AUTH0_REDIRECT_URI })
} else {
const searchParams = new URLSearchParams(window.location.search)
await logIn({
appState: { targetUrl: searchParams.get('redirectTo') },
})
}
}}
>
{isAuthenticated ? 'Log out' : 'Log in'}
</Button>
)
}
```

## Integration

If you're using Auth0 you must also [create an API](https://auth0.com/docs/quickstart/spa/react/02-calling-an-api#create-an-api) and set the audience parameter, or you'll receive an opaque token instead of a JWT token, and Redwood expects to receive a JWT token.

### Role-Based Access Control (RBAC)

[Role-based access control (RBAC)](https://auth0.com/docs/authorization/concepts/rbac) refers to the idea of assigning permissions to users based on their role within an organization. It provides fine-grained control and offers a simple, manageable approach to access management that is less prone to error than assigning permissions to users individually.

Essentially, a role is a collection of permissions that you can apply to users. A role might be "admin", "editor" or "publisher". This differs from permissions an example of which might be "publish:blog".

### App Metadata

Auth0 stores information (such as, support plan subscriptions, security roles, or access control groups) in `app_metadata`. Data stored in `app_metadata` cannot be edited by users.

Create and manage roles for your application in Auth0's "User & Role" management views. You can then assign these roles to users.

However, that info is not immediately available on the user's `app_metadata` or to RedwoodJS when authenticating.

If you assign your user the "admin" role in Auth0, you will want your user's `app_metadata` to look like:

```
{
"roles": [
"admin"
]
}
```

To set this information and make it available to RedwoodJS, you can use [Auth0 Rules](https://auth0.com/docs/rules).

### Rules for App Metadata

RedwoodJS needs `app_metadata` to 1) contain the role information and 2) be present in the JWT that is decoded.

To accomplish these tasks, you can use [Auth0 Rules](https://auth0.com/docs/rules) to add them as custom claims on your JWT.

#### Add Authorization Roles to App Metadata Rule

Your first rule will `Add Authorization Roles to App Metadata`.

```jsx
/// Add Authorization Roles to App Metadata
function (user, context, callback) {
auth0.users.updateAppMetadata(user.user_id, context.authorization)
.then(function(){
callback(null, user, context);
})
.catch(function(err){
callback(err);
});
}
```

Auth0 exposes the user's roles in `context.authorization`. This rule simply copies that information into the user's `app_metadata`, such as:

```
{
"roles": [
"admin"
]
}
```

However, now you must include the `app_metadata` on the user's JWT that RedwoodJS will decode.

#### Add App Metadata to JWT Rule

Therefore, your second rule will `Add App Metadata to JWT`.

You can add `app_metadata` to the `idToken` or `accessToken`.

Adding to `idToken` will make the make app metadata accessible to RedwoodJS `getUserMetadata` which for Auth0 calls the auth client's `getUser`.

Adding to `accessToken` will make the make app metadata accessible to RedwoodJS when decoding the JWT via `getToken`.

While adding to `idToken` is optional, you _must_ add to `accessToken`.

To keep your custom claims from colliding with any reserved claims or claims from other resources, you must give them a [globally unique name using a namespaced format](https://auth0.com/docs/tokens/guides/create-namespaced-custom-claims). Otherwise, Auth0 will _not_ add the information to the token(s).

Therefore, with a namespace of "https://example.com", the `app_metadata` on your token should look like:

```jsx
"https://example.com/app_metadata": {
"authorization": {
"roles": [
"admin"
]
}
},
```

To set this namespace information, use the following function in your rule:

```jsx
function (user, context, callback) {
var namespace = 'https://example.com/';

// adds to idToken, i.e. userMetadata in RedwoodJS
context.idToken[namespace + 'app_metadata'] = {};
context.idToken[namespace + 'app_metadata'].authorization = {
groups: user.app_metadata.groups,
roles: user.app_metadata.roles,
permissions: user.app_metadata.permissions
};

context.idToken[namespace + 'user_metadata'] = {};

// accessToken, i.e. the decoded JWT in RedwoodJS
context.accessToken[namespace + 'app_metadata'] = {};
context.accessToken[namespace + 'app_metadata'].authorization = {
groups: user.app_metadata.groups,
roles: user.app_metadata.roles,
permissions: user.app_metadata.permissions
};

context.accessToken[namespace + 'user_metadata'] = {};

return callback(null, user, context);
}
```

Now, your `app_metadata` with `authorization` and `role` information will be on the user's JWT after logging in.

### Application `hasRole` Support

If you intend to support, RBAC then in your `api/src/lib/auth.js` you need to extract `roles` using the `parseJWT` utility and set these roles on `currentUser`.

If your roles are on a namespaced `app_metadata` claim, then `parseJWT` provides an option to provide this value.

```jsx title="api/src/lib/auth.js"
const NAMESPACE = 'https://example.com'

const currentUserWithRoles = async (decoded) => {
const currentUser = await userByUserId(decoded.sub)
return {
...currentUser,
roles: parseJWT({ decoded: decoded, namespace: NAMESPACE }).roles,
}
}

export const getCurrentUser = async (decoded, { type, token }) => {
try {
requireAccessToken(decoded, { type, token })
return currentUserWithRoles(decoded)
} catch (error) {
return decoded
}
}
```
Loading

0 comments on commit 358dc83

Please sign in to comment.