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

Remove magicAuthLink and passwordResetLink functionality from @keystone-6/auth #9245

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
5 changes: 5 additions & 0 deletions .changeset/rm-auth-magic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/auth': major
---

Remove `magicAuthLink` and `passwordResetLink` functionality from `@keystone-6/auth`
259 changes: 9 additions & 250 deletions docs/content/docs/config/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ Additional options to this function provide support for creating an initial item
For examples of how to use authentication in your system please see the [authentication guide](../guides/auth-and-access-control).

```typescript
import { config, list } from '@keystone-6/core';
import { text, password, checkbox } from '@keystone-6/core/fields';
import { createAuth } from '@keystone-6/auth';
import { config, list } from '@keystone-6/core'
import { text, password, checkbox } from '@keystone-6/core/fields'
import { createAuth } from '@keystone-6/auth'

const { withAuth } = createAuth({
// Required options
Expand All @@ -26,15 +26,7 @@ const { withAuth } = createAuth({
itemData: { isAdmin: true },
skipKeystoneWelcome: false,
},
passwordResetLink: {
sendToken: async ({ itemId, identity, token, context }) => { /* ... */ },
tokensValidForMins: 60,
},
magicAuthLink: {
sendToken: async ({ itemId, identity, token, context }) => { /* ... */ },
tokensValidForMins: 60,
},
});
})

export default withAuth(
config({
Expand All @@ -49,7 +41,7 @@ export default withAuth(
session: { /* ... */ },
},
})
);
)
```

The function `createAuth` returns a function `withAuth` which should be used to wrap your `config()`.
Expand All @@ -65,13 +57,13 @@ The core functionality of the authentication system provides a GraphQL mutation
- `secretField`: The name of the field to use as a secret. This field must be a `password()` field type.

```typescript
import { createAuth } from '@keystone-6/auth';
import { createAuth } from '@keystone-6/auth'

const { withAuth } = createAuth({
listKey: 'User',
identityField: 'email',
secretField: 'password',
});
})
```

#### GraphQL API {% #graphql-api %}
Expand Down Expand Up @@ -156,14 +148,14 @@ Configuring `sessionData` will add an `session.data` based on the `itemId`, popu
The value is a GraphQL query string which indicates which fields should be populated on the `session.data` object

```typescript
import { createAuth } from '@keystone-6/auth';
import { createAuth } from '@keystone-6/auth'

const { withAuth } = createAuth({
listKey: 'User',
identityField: 'email',
secretField: 'password',
sessionData: 'id name isAdmin',
});
})
```

### initFirstItem
Expand Down Expand Up @@ -221,239 +213,6 @@ This mutation is used by the Admin UI's initial user screen and should generally

The initial user screen is added at `/init`, and users are redirected here if there is no active session and no users in the system.

### passwordResetLink

This option adds support for sending password reset links to users.
The mutation `sendUserPasswordResetLink` allows you to send a reset token to a user.
The mutation `redeemUserPasswordResetToken` lets the user reset their password by redeeming the token.
You need to provide a `sendToken` function which can be used by `sendUserPasswordResetLink` to send the generated token to the user.
It is expected that you will use these mutations as part of a password reset workflow within your frontend application.

#### Options {% #password-reset-link-options %}

- `sendToken`: This function is invoked by the `sendUserPasswordResetLink` mutation.
It should use an appropriate mechanism (e.g email, Twitter, Slack, carrier pigeon) to provide the user with the password reset token.
It should include an appropriate way to submit the token to the `redeemUserPasswordResetToken` mutation (e.g. a link to a password reset form).
The following arguments are provided to `sendToken`:
- `itemId`: The ID of the user requesting the password reset.
- `identity`: The identity value provided to the `sendUserPasswordResetLink` mutation.
- `token`: The token the user must supply to use `redeemUserPasswordResetToken`.
- `context`: A [`KeystoneContext`](../context/overview) object.
- `tokensValidForMins` (default: `10`, max: `24 * 60` (1 day), min: `0.16` (10 seconds)): The length of time, in minutes, that the token is valid for.

```typescript
import { createAuth } from '@keystone-6/auth';

const { withAuth } = createAuth({
listKey: 'User',
identityField: 'email',
secretField: 'password',

passwordResetLink: {
sendToken: async ({ itemId, identity, token, context }) => { /* ... */ },
tokensValidForMins: 60,
},
});
```

#### Additional fields

Enabling `passwordResetLink` will add the following fields to the configuration of the list `listKey`.

```typescript
const fieldConfig = {
access: () => false,
ui: {
createView: { fieldMode: 'hidden' },
itemView: { fieldMode: 'hidden' },
listView: { fieldMode: 'hidden' },
},
} as const;
const fields = {
passwordResetToken: password(fieldConfig),
passwordResetIssuedAt: timestamp(fieldConfig),
passwordResetRedeemedAt: timestamp(fieldConfig),
};
```

- `passwordResetToken` stores the token generated by `sendUserPasswordResetLink`.
- `passwordResetIssuedAt` records the time that the token was generated.
- `passwordResetRedeemedAt` records the time that the token was redeemed.

#### GraphQL API {% #password-reset-graphql-api %}

Enabling `passwordResetLink` will add the following elements to the GraphQL API.

```graphql
type Mutation {
sendUserPasswordResetLink(email: String!): Boolean
redeemUserPasswordResetToken(email: String!, token: String!, password: String!): RedeemUserPasswordResetTokenResult
}

type Query {
validateUserPasswordResetToken(email: String!, token: String!): ValidateUserPasswordResetTokenResult
}

type ValidateUserPasswordResetTokenResult {
code: PasswordResetRedemptionErrorCode!
message: String!
}

type RedeemUserPasswordResetTokenResult {
code: PasswordResetRedemptionErrorCode!
message: String!
}

enum PasswordResetRedemptionErrorCode {
FAILURE
TOKEN_EXPIRED
TOKEN_REDEEMED
}
```

##### sendUserPasswordResetLink

This mutation verifies that the supplied identity exists and, if it does, generates a new token and calls `sendToken()`.
The token and the the current time are stored in the fields `passwordResetToken` and `passwordResetIssuedAt` respectively.
The argument name for this function is the value of `identityField`.
This mutation always returns `null`.

##### redeemUserPasswordResetToken

This mutation validates the provided token and then resets the user's password.
The argument names for this function are the values of `identityField` and `secretField`.
This mutation returns `null` on success.

If the provided `identity` and `token` do not match then the value `{ code: FAILURE, message: 'Auth token redemption failed.'}` is returned.

If the `identity` and `token` values match, but the value of `passwordResetRedeemedAt` on the item is not `null` then the value `{ code: TOKEN_REDEEMED, message: 'Auth tokens are single use and the auth token provided has already been redeemed.' }` is returned.

If the `identity` and `token` values match and the token has not already been redeemed then the value of `passwordResetIssuedAt` is compared against `tokensValidForMins`.
If the token has expired the value `{ code: TOKEN_EXPIRED, message: 'The auth token provided has expired.' }` is returned.

If the token is valid, then the value of `passwordResetRedeemedAt` will be set to the current time, and then the new password value will be saved.
The password will be validated before being saved.
If the password is invalid the token will still be considered as redeemed and the user must restart the password reset flow.
A `ValidationFailureError` will be returned as an `error` if the password is invalid.

If the password is successfully saved then the mutation will return `null`.

##### validateUserPasswordResetToken

This query performs all the same validation steps as `redeemUserPasswordResetToken`, but does not update the password or `passwordResetRedeemedAt` field.
The return values are the same as `redeemUserPasswordResetToken`.

### magicAuthLink

This option adds support for sending a one-time authentication link to users.
One-time authentication links allow a user to start an authenticated session without needing to know their password.
The mutation `sendUserMagicAuthLink` allows you to send a one-time authentication link token to a user.
The mutation `redeemUserMagicAuthToken` lets the user start an authenticated session by redeeming the token.
You need to provide a `sendToken` function which can be used by `sendUserMagicAuthLink` to send the generated token to the user.
It is expected that you will use these mutations as part of a one-time authentication workflow within your frontend application.

#### Options {% #magic-auth-link-options %}

- `sendToken`: This function is invoked by the `sendUserMagicAuthLink` mutation.
It should use an appropriate mechanism (e.g email, Twitter, Slack, carrier pigeon) to provide the user with their one-time authentication token.
It should include an appropriate way to submit the token to the `redeemUserMagicAuthToken` mutation (e.g. a link to a route which executes the mutation on their behalf).
The following arguments are provided to `sendToken`:
- `itemId`: The ID of the user requesting the one-time authentication link.
- `identity`: The identity value provided to the `sendUserMagicAuthLink` mutation.
- `token`: The token the user must supply to use `redeemUserMagicAuthToken`.
- `context`: A [`KeystoneContext`](../context/overview) object.
- `tokensValidForMins` (default: `10`, max: `24 * 60` (1 day), min: `0.16` (10 seconds)): The length of time, in minutes, that the token is valid for.

```typescript
import { createAuth } from '@keystone-6/auth';

const { withAuth } = createAuth({
listKey: 'User',
identityField: 'email',
secretField: 'password',

magicAuthLink: {
sendToken: async ({ itemId, identity, token, context }) => { /* ... */ },
tokensValidForMins: 60,
},
});
```

#### Additional fields {% #magic-auth-link-additional-fields %}

Enabling `magicAuthLink` will add the following fields to the configuration of the list `listKey`.

```typescript
const fieldConfig = {
access: () => false,
ui: {
createView: { fieldMode: 'hidden' },
itemView: { fieldMode: 'hidden' },
listView: { fieldMode: 'hidden' },
},
} as const;
const fields = {
magicAuthToken: password(fieldConfig),
magicAuthIssuedAt: timestamp(fieldConfig),
magicAuthRedeemedAt: timestamp(fieldConfig),
};
```

- `magicAuthToken` stores the token generated by `sendUserMagicAuthLink`.
- `magicAuthIssuedAt` records the time that the token was generated.
- `magicAuthRedeemedAt` records the time that the token was redeemed.

#### GraphQL API

Enabling `magicAuthLink` will add the following elements to the GraphQL API.

```graphql
type Mutation {
sendUserMagicAuthLink(email: String!): Boolean
redeemUserMagicAuthToken(email: String!, token: String!): RedeemUserMagicAuthTokenResult!
}

union RedeemUserMagicAuthTokenResult = RedeemUserMagicAuthTokenSuccess | RedeemUserMagicAuthTokenFailure

type RedeemUserMagicAuthTokenSuccess {
token: String!
item: User!
}

type RedeemUserMagicAuthTokenFailure {
code: MagicLinkRedemptionErrorCode!
message: String!
}

enum MagicLinkRedemptionErrorCode {
FAILURE
TOKEN_EXPIRED
TOKEN_REDEEMED
}
```

##### sendUserMagicAuthLink

This mutation verifies that the supplied identity exists and, if it does, generates a new token and calls `sendToken()`.
The token and the the current time are stored in the fields `magicAuthToken` and `magicAuthIssuedAt` respectively.
The argument name for this function is the value of `identityField`.
This mutation always returns `null`.

##### redeemUserMagicAuthToken

This mutation validates the provided token and then starts an authenticated session as the identified user.
The argument name for this function is the value of `identityField`.

If the provided `identity` and `token` do not match then the value `{ code: FAILURE, message: 'Auth token redemption failed.'}` is returned.

If the `identity` and `token` values match, but the value of `magicAuthRedeemedAt` on the item is not `null` then the value `{ code: TOKEN_REDEEMED, message: 'Auth tokens are single use and the auth token provided has already been redeemed.' }` is returned.

If the `identity` and `token` values match and the token has not already been redeemed then the value of `magicAuthRedeemedAt` is compared against `tokensValidForMins`.
If the token has expired the value `{ code: TOKEN_EXPIRED, message: 'The auth token provided has expired.' }` is returned.

If the token is valid then the session handler will start a new session and return the encoded session cookie data as `sessionToken`.
The authenticated item will be returned as `item`.

## Related resources

{% related-content %}
Expand Down
34 changes: 34 additions & 0 deletions examples/custom-auth-magiclink/keystone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { config } from '@keystone-6/core'
import { statelessSessions } from '@keystone-6/core/session'
import { fixPrismaPath } from '../example-utils'
import { lists } from './schema'
import type {TypeInfo } from '.keystone/types'

// WARNING: this example is for demonstration purposes only
// as with each of our examples, it has not been vetted
// or tested for any particular usage

// WARNING: you need to change this
const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'

// statelessSessions uses cookies for session tracking
// these cookies have an expiry, in seconds
// we use an expiry of one hour for this example
const sessionMaxAge = 60 * 60

export default config<TypeInfo>({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL ?? 'file:./keystone-example.db',

// WARNING: this is only needed for our monorepo examples, dont do this
...fixPrismaPath,
},
lists,
session: statelessSessions({
// the maxAge option controls how long session cookies are valid for before they expire
maxAge: sessionMaxAge,
// the session secret is used to encrypt cookie data
secret: sessionSecret,
}),
})
22 changes: 22 additions & 0 deletions examples/custom-auth-magiclink/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@keystone-6/example-custom-auth-magiclink",
"version": "0.0.2",
"private": true,
"license": "MIT",
"scripts": {
"dev": "keystone dev",
"start": "keystone start",
"build": "keystone build",
"postinstall": "keystone postinstall"
},
"dependencies": {
"@keystone-6/auth": "workspace:^",
"@keystone-6/core": "workspace:^",
"@prisma/client": "catalog:",
"graphql": "catalog:"
},
"devDependencies": {
"prisma": "catalog:",
"typescript": "catalog:"
}
}
7 changes: 7 additions & 0 deletions examples/custom-auth-magiclink/sandbox.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"template": "node",
"container": {
"startScript": "keystone dev",
"node": "16"
}
}
Loading
Loading