Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Update/upgrade auth #9

Merged
merged 13 commits into from
Oct 15, 2023
5 changes: 5 additions & 0 deletions .changeset/cool-moose-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@imcorfitz/payload-plugin-oauth-apps": minor
---

Removing cookies from plugin feature
5 changes: 5 additions & 0 deletions .changeset/eighty-pigs-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@imcorfitz/payload-plugin-oauth-apps": minor
---

Added logout route and operation
5 changes: 5 additions & 0 deletions .changeset/gold-rats-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@imcorfitz/payload-plugin-oauth-apps": minor
---

Using email settings from Oauth client settings instead of plugin config
5 changes: 5 additions & 0 deletions .changeset/red-numbers-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@imcorfitz/payload-plugin-oauth-apps": minor
---

Added magiclink auth flow
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
- `Session management` on User collections with ability to revoke active sessions
- `Passwordless authentication` using One-time password (OTP) or Magiclink
- Automatically adds registered OAuth apps to `CSRF` and `CORS` config in Payload
- Full support of native Payload Auth cookies and JWT passport strategy

[0.1.1]: https://github.com/imcorfitz/payload-plugin-oauth-apps

Expand Down
90 changes: 70 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
- Ability to create multiple `OAuth Apps` with individual client credentials
- Better session flow using revokable longer-lived `refresh tokens`
- `Session management` on User collections with ability to revoke active sessions
- `Passwordless authentication` using One-time password (OTP) or Magiclink (Coming soon)
- `Passwordless authentication` using One-time password (OTP) or Magiclink
- Automatically adds registered OAuth apps to `CSRF` and `CORS` config in Payload
- Full support of native Payload Auth cookies and JWT passport strategy

## Installation

Expand Down Expand Up @@ -70,24 +69,23 @@ export default buildConfig({

- `authorization`: object | optional

Configure how `OAuth Apps` authorize users and initialize new sessions. The default `method` is 'crednetials'.
Configure how `OAuth Apps` authorize users and initialize new sessions.

- `method`: 'credentials' | 'otp' | 'magiclink' | 'custom' | optional
- `customHandler`: EndpointHandler | optional
- `customHandlers`: {<custom_method>: EndpointHandler} | optional
- `otpExpiration`: number | optional
- `generateOTP`: method | optional
- `generateEmailHTML`: method | optional
- `generateEmailSubject`: method | optional
- `generateEmailVariables`: method | optional

When using `otp` and authorization method, you can set the expiration (`otpExpiration` - defaults to 10 minutes) and customise how you want the one-time password to be generated (`generateOTP` - defaults to generating a 6-digit number).

Both `magiclink` (Coming soon) and `otp` allows you to set the `generateEmailHTML` and `generateEmailSubject` methods to customise the email sent to the user for authentication. In both method you will have access to following properties:
Both `magiclink` and `otp` allows you to set the `generateEmailVariables` method to customise the email variables available in the OAuth App settings. In both method you will have access to following properties:

- `req`: PayloadRequest
- `token`: The generated OTP or an encrypted token depending on the set method
- `variables`: An object containing a magiclink and token, or an OTP, depending on the `method`
- `user`: Information about the user to be authenticated
- `client`: Details about the OAuth App making the auth request

> Note: `customHandler` should be set if `method` is set to 'custom' and allows you to perform the entire authentication flow yourself. Note that the plugin does expose the generateAccessToken and generateRefreshToken methods, however this goes beyond the scope of this documentation, and should be used in advance cases only.
> Note: `customHandlers` should be set if you wish to create your own `method` and allows you to perform the entire authentication flow yourself. Note that the plugin does expose the generateAccessToken and generateRefreshToken methods, however this goes beyond the scope of this documentation, and should be used in advance cases only.

- `sessions`: object | optional

Expand Down Expand Up @@ -138,22 +136,24 @@ const Admins: CollectionConfig = {

- [POST] `oauth/authorize`:

Used by OAuth apps to log in users. Upon sucessful login, the response will contain an access token and a refresh token.
Used by OAuth apps to log in users. Upon sucessful login, the response will contain an access token and a refresh token. By passing `method` as part of the body, you can tell Payload CMS how you wish to authenticate the user. The plugin support `credentials`, `otp`, and `magiclink` out of the box.

> Note: Don't ever expose your client id or client secret to the client. These operations should always be made securely from server-side.

| Parameter | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| email `required` | The email address of the user to be logged in. |
| password | The password of the user to be logged in. _NB: `required` if `authorization.method` is set to 'credentials'_ |
| email `required` | The email address of the user to be logged in |
| clientId `required` | The client id of the OAuth App performing the operation |
| clientSecret `required` | The client secret of the OAuth App performing the operation |
| method | `'credentials' \| 'otp' \| 'magiclink' \| <custom_method>.` The default `method` is 'credentials' |
| password | The password of the user to be logged in. _NB: `required` if `authorization.method` is set to 'credentials'_ |

```ts
// Request
const response = await fetch(`https://my.payloadcms.tld/<user-collection>/oauth/authorize`, {
method: 'POST',
body: JSON.stringify({
method: "credentials",
email: "[email protected]",
password: "very-safe-password-1234",
clientId: "CID_s3o8y384y5...",
Expand All @@ -176,11 +176,11 @@ const Admins: CollectionConfig = {

> Note: Don't ever expose your client id or client secret to the client. These operations should always be made securely from server-side.

| Parameter | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| refreshToken | Only `required` if cookie authentication isn't enable for the OAuth app. Otherwise passing the cookies with the request will suffice |
| clientId `required` | The client id of the OAuth App performing the operation |
| clientSecret `required` | The client secret of the OAuth App performing the operation |
| Parameter | Description |
| ----------------------- | ----------------------------------------------------------- |
| refreshToken `required` | The refresh token issued at authorization |
| clientId `required` | The client id of the OAuth App performing the operation |
| clientSecret `required` | The client secret of the OAuth App performing the operation |

```ts
// Request
Expand All @@ -202,13 +202,13 @@ const Admins: CollectionConfig = {

- [POST] `oauth/verify-otp`:

When `authorization.method` is set to 'otp', the user will receive an email with a one-time password. Use this endpoint to finalize the authentication process and receive an access and refresh token.
When `method` is set to 'otp', the user will receive an email with a one-time password. Use this endpoint to finalize the authentication process and receive an access and refresh token.

> Note: Don't ever expose your client id or client secret to the client. These operations should always be made securely from server-side.

| Parameter | Description |
| ----------------------- | ----------------------------------------------------------- |
| email `required` | The email address of the user to be logged in. |
| email `required` | The email address of the user to be logged in |
| otp `required` | The one-time password received by the user by email |
| clientId `required` | The client id of the OAuth App performing the operation |
| clientSecret `required` | The client secret of the OAuth App performing the operation |
Expand All @@ -234,6 +234,56 @@ const Admins: CollectionConfig = {
}
```

- [GET] `oauth/verify-magiclink`:

When `method` is set to 'magiclink', the user will receive an email with a link. The link is directing the user to this endpoint by default (if not overridden in OAuth App settings). When validated, the user will be redirected to the callbackUrl registered for the OAuth App.

| Query Parameter | Description |
| ---------------- | --------------------------------------- |
| token `required` | The token received by the user by email |

- [POST] `oauth/verify-magiclink`:

Same endpoint can also be used by an OAuth app to post the user's token for validation. Same process applies, but instead of a redirect, the call will output a JSON object with the status of the validation.

| Parameter | Description |
| ---------------- | --------------------------------------- |
| token `required` | The token received by the user by email |

- [POST] `oauth/verify-code`:

When `method` is set to 'magiclink' and the user has clicked the link they've received calling this endpoint with the code received at the authentication call, this endpoint will verify your code and finalize the authentication process and issue an access and refresh token.

> Note: Don't ever expose your client id or client secret to the client. These operations should always be made securely from server-side.

| Parameter | Description |
| ----------------------- | ----------------------------------------------------------- |
| email `required` | The email address of the user to be logged in |
| code `required` | The code received during authentication call |
| clientId `required` | The client id of the OAuth App performing the operation |
| clientSecret `required` | The client secret of the OAuth App performing the operation |

```ts
// Request
const response = await fetch(`https://my.payloadcms.tld/<user-collection>/oauth/verify-code`, {
method: 'POST',
body: JSON.stringify({
email: "[email protected]",
code: "AbCdEf123456",
clientId: "CID_s3o8y384y5...",
clientSecret: "CS_skijorintg..."
})
})

// Successful Response
{
"accessToken": "eyJhbGciOiJIUzI1N...XMnxpb1NTK9K0",
"accessExpiration": 3600,
"refreshToken": "43d5cc1ee66ac880...94b8f2df",
"refreshExpiration": 2592000
}
```

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
Expand Down
3 changes: 0 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# To Do

- [ ] Create magiclink auth flow
- [ ] Add custom generate security pass phrase function
- [ ] Overwrite Graphql APIs / Introduce new ones
- [ ] Login
- [ ] Logout
- [ ] Refresh token
- [ ] Write more documentation
- [ ] Test support for vite bundler and Postgres DB adapter
- [ ] Write tests
- [ ] Bin cookies from the auth process
34 changes: 18 additions & 16 deletions demo/src/payload.config.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { slateEditor } from '@payloadcms/richtext-slate'
import { webpackBundler } from '@payloadcms/bundler-webpack'
import { buildConfig } from 'payload/config'
import path from 'path'
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import { slateEditor } from "@payloadcms/richtext-slate";
import { webpackBundler } from "@payloadcms/bundler-webpack";
import { buildConfig } from "payload/config";
import path from "path";
// import Examples from './collections/Examples';
import Users from './collections/Users'
import Users from "./collections/Users";

// import { oAuthApps } from '../../dist';
// eslint-disable-next-line import/no-relative-packages
import { oAuthApps } from '../../src'
import { oAuthApps } from "../../src";

export default buildConfig({
serverURL: 'http://localhost:3030',
serverURL: "http://localhost:3030",
admin: {
user: Users.slug,
bundler: webpackBundler(),
},
email: {
fromName: 'Admin',
fromAddress: '[email protected]',
fromName: "Admin",
fromAddress: "[email protected]",
logMockCredentials: true, // Optional
},
editor: slateEditor({}),
Expand All @@ -30,24 +30,26 @@ export default buildConfig({
// Examples,
],
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
outputFile: path.resolve(__dirname, "payload-types.ts"),
},
graphQL: {
schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"),
},
db: mongooseAdapter({
url: process.env.MONGODB_URI,
}),
plugins: [
oAuthApps({
userCollections: [Users.slug],
// authorization: {
// generateEmailVariables: () => ({
// test: "testing a longer sentence.. Maybe wiht emojies? 🤣",
// }),
// },
sessions: {
limit: 4,
ipinfoApiKey: process.env.IPINFO_API_KEY,
},
// authorization: {
// method: "otp",
// },
access: {
sessions: {
read: () => true,
Expand All @@ -56,4 +58,4 @@ export default buildConfig({
},
}),
],
})
});
78 changes: 62 additions & 16 deletions src/collections/OAuthApps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,9 @@ export const OAuthApps: CollectionConfig = {
required: true,
admin: {
description:
'When using magiclink, this is the URL that the user will be redirected to after they have authenticated. The callback URL will receive a query parameter called `token` which can be used in exchange for an access and refresh token.',
'When using magiclink, this is the URL that the user will be redirected to after they have authenticated.',
},
},
{
type: 'row',
fields: [
{
type: 'checkbox',
name: 'enableCookies',
label: 'Enable Cookies',
defaultValue: false,
admin: {
description:
'This will create responsoe cookies when the user authenticates as well as add the hostname to the list of allowed origins for CSRF.',
},
},
],
},
{
type: 'group',
name: 'credentials',
Expand Down Expand Up @@ -145,5 +130,66 @@ export const OAuthApps: CollectionConfig = {
},
],
},
{
type: 'group',
name: 'settings',
label: 'Settings',
fields: [
{
type: 'checkbox',
name: 'customizeOtpEmail',
label: 'Customize OTP Email',
defaultValue: false,
},
{
type: 'text',
name: 'otpEmailSubject',
label: 'OTP Email Subject',
admin: {
condition: (_, siblingData) => siblingData?.customizeOtpEmail,
description:
'This is the subject that will be sent in the email when using OTP authentication. You have access to the variables `{{otp}}` and `{{email}}` - and any additional variables made available by the administrator.',
},
},
{
type: 'code',
name: 'otpEmail',
label: 'OTP Email',
admin: {
language: 'html',
condition: (_, siblingData) => siblingData?.customizeOtpEmail,
description:
'This is the HTML that will be sent in the email when using OTP authentication. You have access to the variables `{{otp}}` and `{{email}}` - and any additional variables made available by the administrator.',
},
},
{
type: 'checkbox',
name: 'customizeMagiclinkEmail',
label: 'Customize Magiclink Email',
defaultValue: false,
},
{
type: 'text',
name: 'magiclinkEmailSubject',
label: 'Magiclink Email Subject',
admin: {
condition: (_, siblingData) => siblingData?.customizeMagiclinkEmail,
description:
'This is the subject that will be sent in the email when using magiclink authentication. You have access to the variables `{{magiclink}}` and `{{email}}` - and any additional variables made available by the administrator.',
},
},
{
type: 'code',
name: 'magiclinkEmail',
label: 'Magiclink Email',
admin: {
language: 'html',
condition: (_, siblingData) => siblingData?.customizeMagiclinkEmail,
description:
'This is the HTML that will be sent in the email when using magiclink authentication. You have access to the variables `{{magiclink}}` and `{{email}}` - and any additional variables made available by the administrator.',
},
},
],
},
],
}
Loading