diff --git a/.changeset/good-pumas-scream.md b/.changeset/good-pumas-scream.md new file mode 100644 index 0000000..32b8669 --- /dev/null +++ b/.changeset/good-pumas-scream.md @@ -0,0 +1,5 @@ +--- +"@imcorfitz/payload-plugin-oauth-apps": patch +--- + +Updated documentation diff --git a/.prettierrc.js b/.prettierrc.js index 70c17c9..8bf2ba1 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,8 +1,15 @@ module.exports = { - printWidth: 100, - parser: "typescript", - semi: false, - singleQuote: true, - trailingComma: "all", - arrowParens: "avoid", + overrides: [ + { + files: ["src/**/*.ts", "src/**/*.tsx"], + options: { + printWidth: 100, + parser: "typescript", + semi: false, + singleQuote: true, + trailingComma: "all", + arrowParens: "avoid", + }, + }, + ], }; diff --git a/README.md b/README.md index 3bdde8f..9c1a1c5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ NPM -> Please note this plugin is under active development. A lot `WILL` change. see [TODO](TODO.md) +> Please note this plugin is under active development. A lot `WILL` change. Please see [TODO](TODO.md) ## Features @@ -29,9 +29,11 @@ npm install @imcorfitz/payload-plugin-oauth-apps yarn add @imcorfitz/payload-plugin-oauth-apps ``` +### Requirements -## Usage +- Payload ^2.0.0 +## Usage ### Setup plugin @@ -47,12 +49,66 @@ export default buildConfig({ userCollections: [Users.slug], }), ], -}) +}); ``` +### Options + +- `userCollections`: string[] | required + + An array of collections slugs to enable OAuth Sessions. Enabled collections receive an `OAuth` group with a sessions array, listing all currently active sessions. + +- `access`: object | optional + + Allows you to configure field-level access control on the fields in the `OAuth` group on configured user collections. + + - `sessions`: object | optional + - `read`: FieldAccess | optional + - `create`: FieldAccess | optional + - `update`: FieldAccess | optional + +- `authorization`: object | optional + + Configure how `OAuth Apps` authorize users and initialize new sessions. The default `method` is 'crednetials'. + + - `method`: 'credentials' | 'otp' | 'magiclink' | 'custom' | optional + - `customHandler`: EndpointHandler | optional + - `otpExpiration`: number | optional + - `generateOTP`: method | optional + - `generateEmailHTML`: method | optional + - `generateEmailSubject`: method | optional + + When using `otp` and aothorization 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: + + - `req`: PayloadRequest + - `token`: The generated OTP or an encrypted token depending on the set method + - `user`: Information about the user to be authenticated + + > 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. + +- `sessions`: object | optional + + Configuration of the sessions created. + + - `limit`: number | optional + - `ipinfoApiKey`: string | optional + - `fetchLocationInfo`: method | optional + - `refreshTokenExpiration`: number | optional + + Allows you set a `limit` of number of sessions per user. If not set, users are free to create unlimited sessions (not adviced). When set, oldest session will be removed when limit has been reached and a new session is initialised. + + By default all refresh tokens have a lifespan of 30 days. You can override this by passing `refreshTokenExpiration` with the amount of seconds a refresh token should be valid for. + + The plugin uses [`IPInfo`](https://ipinfo.io/) to fetch location information whenever a session is created. To use this, simply set your own `ipinfoApiKey`. If you wish to use an alternative location detection service, feel free to use the `fetchLocationInfo` method which gives you following properties: + + - `req`: PayloadRequest + - `ip`: The detected IP address | possibly undefined + ### Add OAuth Manager -Add the `oAuthManager` field to your admin user collection. +Add the `oAuthManager` field to your admin user collection. This determines which users have access to manage OAuth Apps in Payload CMS. ```ts // collections/admins.ts @@ -65,6 +121,7 @@ const Admins: CollectionConfig = { fields: [ // ... Other fields oAuthManager({ + // NOTE: You can pass Checkbox field properties here to override all field properties except for: name, label and type. access: { update: isAdminFieldLevel, }, @@ -76,6 +133,106 @@ const Admins: CollectionConfig = { }; ``` +## OAuth REST API endpoints + +- [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. + + > 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'_ | + | 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//oauth/authorize`, { + method: 'POST', + body: JSON.stringify({ + email: "user@payloadcms.com", + password: "very-safe-password-1234", + clientId: "CID_s3o8y384y5...", + clientSecret: "CS_skijorintg..." + }) + }) + + // Successful Response + { + "accessToken": "eyJhbGciOiJIUzI1N...XMnxpb1NTK9K0", + "accessExpiration": 3600, + "refreshToken": "43d5cc1ee66ac880...94b8f2df", + "refreshExpiration": 2592000 + } + ``` + +- [POST] `oauth/refresh-token`: + + Used by OAuth apps to request a new access token using their issued refresh token. Upon sucessful login, the response will contain an access token and a 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 | + | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | + | 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 | + + ```ts + // Request + const response = await fetch(`https://my.payloadcms.tld//oauth/refresh-token`, { + method: 'POST', + body: JSON.stringify({ + refreshToken: "43d5cc1ee66ac880...94b8f2df", + clientId: "CID_s3o8y384y5...", + clientSecret: "CS_skijorintg..." + }) + }) + + // Successful Response + { + "accessToken": "eyJhbGciOiJIUAhd7...XMnxpVbUoAyhI", + "accessExpiration": 3600, + } + ``` + +- [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. + + > 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. | + | 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 | + + ```ts + // Request + const response = await fetch(`https://my.payloadcms.tld//oauth/verify-otp`, { + method: 'POST', + body: JSON.stringify({ + email: "user@payloadcms.com", + otp: "123456", + 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. @@ -83,8 +240,19 @@ Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recen ## Known issues ### Reset password + Currently Payload doesn't feature operation hooks on `reset password`, and it automatically initialises a session and issues an access token when the operation is done. This is not a problem when operating within the CMS; however, it doesn't allow for this plugin to limit the session creation to be CMS-only – meaning that an OAuth application is all good to use the `reset password` REST endpoint and GraphQL mutation native to Payload, but this will only create an access token, that will be shortlived and not accompanied by a refresh token. It is therefor adviced for OAuth applications to disregard the session and access token issued by Payload post `reset password` and request the user to log in again after the password has been reset. +## Disclaimer + +### Payload 2.0 + +This plugin was initially written to work with Payload ^1.0.0. An effort has been made to match ^2.0.0, thus leaving behind the legacy ^1.0.0 versions. It should be working fine however, I have yet to test the plugin using the `vite-bundler` and `postgres` db adapter. + +### GraphQL + +The entire auth, refresh and logout flow is fully working using the REST api. I ahve yet to create dedicated GraphQL mutations and resolvers. This is in the works. + ## Contributing Contributions and feedback are very welcome. @@ -105,4 +273,3 @@ To get it running: The MIT License (MIT). Please see [License File](LICENSE) for more information. [link-contributors]: ../../contributors -