Skip to content

Commit 3ef518e

Browse files
authored
feat(#8): add support for password grant (#22)
* feat(#8): add WIP password grant * chore: update gitpod xdebug settings * chore(#8): update README * chore(#8): update local environment and errors * chore(#8): update password grant * chore(#8): update password grant user info * chore(#8): update documentation * chore(#8): fix broken test * chore(#8): update example in readme * chore(#8): update test coverage * chore(#8): add changeset
1 parent 5bab97a commit 3ef518e

12 files changed

Lines changed: 329 additions & 43 deletions

File tree

.changeset/sour-boxes-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"druxt-auth": minor
3+
---
4+
5+
feat(#8): added support for password grant.

.gitpod.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ ports:
7777
- port: 8889
7878
onOpen: ignore
7979
# xdebug port
80-
- port: 9000
80+
- port: 9003
8181
onOpen: ignore
8282

8383
github:

.vscode/launch.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Listen for XDebug",
9+
"type": "php",
10+
"request": "launch",
11+
"hostname": "0.0.0.0",
12+
"port": 9003,
13+
"pathMappings": {
14+
"/var/www/html": "${workspaceRoot}/example/drupal"
15+
}
16+
}
17+
]
18+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 DruxtJS
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,36 +23,59 @@ Add module to `nuxt.config.js`
2323

2424
```js
2525
module.exports = {
26-
buildModules: ['druxt-auth'],
26+
buildModules: [
27+
'druxt',
28+
['druxt-auth', {
29+
clientId: '[DRUPAL_CONSUMER_UUID]',
30+
clientSecret: '[DRUPAL_CONSUMER_SECRET]',
31+
}]
32+
],
2733
druxt: {
28-
baseUrl: 'https://demo-api.druxtjs.org',
29-
auth: {
30-
clientId: '[DRUPAL_CONSUMER_UUID]'
31-
},
34+
baseUrl: 'https://demo-api.druxtjs.org'
3235
},
3336
}
3437
```
3538

36-
_Note:_ Replace `[DRUPAL_CONSUMER_UUID]` with the UUID of the consumer created in the following step.
39+
_Note:_ Replace `[DRUPAL_CONSUMER_UUID]` and `[DRUPAL_CONSUMER_SECRET]` with the details from the consumer created in the following step.
3740

3841
### Drupal
3942

4043
1. Download, install and setup the [Simple OAuth module](https://www.drupal.org/project/simple_oauth).
41-
2. Create a Consumer with:
42-
- New Secret: _leave this empty_
43-
- Is Confidential: _unchecked_
44-
- Use PKCE?: _checked_
45-
- Redirect URI: `[FRONTEND_URL]/callback` (e.g., `http://localhost:3000/callback`)
44+
2. Create a Consumer depending on your desired authorization strategy:
45+
46+
- **Authorization Code** grant:
47+
- New Secret: _leave this empty_
48+
- Is Confidential: _unchecked_
49+
- Use PKCE?: _checked_
50+
- Redirect URI: `[FRONTEND_URL]/callback` (e.g., `http://localhost:3000/callback`)
51+
52+
- **Password** grant:
53+
- New Secret: _provide a secure secret_
54+
- Is Confidential: _checked_
55+
- Redirect URI: `[FRONTEND_URL]/callback` (e.g., `http://localhost:3000/callback`)
4656

4757
## Usage
4858

4959
The DruxtAuth module installs and configures the **nuxt/auth** module for your Druxt site.
5060

51-
It adds a `drupal-authorization_code` auth strategy that can be used via the `$auth` plugin:
61+
It adds two auth strategies that can be used via the `$auth` plugin:
62+
- `drupal-authorization_code`
63+
```js
64+
this.$nuxt.$auth.loginWith('drupal-authorization_code')
65+
```
66+
67+
- `drupal-password`
68+
```js
69+
this.$nuxt.$auth.loginWith('drupal-password', {
70+
data: {
71+
username: '',
72+
password: ''
73+
}
74+
})
75+
```
76+
77+
_Note:_ Nuxt must be running in SSR mode for password grant, and client secret must be set.
5278

53-
```js
54-
this.$nuxt.$auth.loginWith('drupal-authorization_code')
55-
```
5679

5780
- See the **nuxt/auth** documentation form more details: https://auth.nuxtjs.org/api/auth
5881

@@ -61,4 +84,5 @@ this.$nuxt.$auth.loginWith('drupal-authorization_code')
6184

6285
| Option | Type | Required | Default | Description |
6386
| --- | --- | --- | --- | --- |
64-
| `druxt.auth.cliendId` | `string` | Yes | `undefined` | The Drupal Consumer UUID |
87+
| `clientId` | `string` | Yes | `undefined` | The Drupal Consumer UUID |
88+
| `clientSecret` | `string` | No | `undefined` | The Drupal Consumer API secret. Required for Password grant. |

example/nuxt/nuxt.config.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require('dotenv').config({ path: '../.env' })
2-
const baseUrl = process.env.BASE_URL || 'https://druxt-auth.ddev.site'
2+
const baseUrl = process.env.BASE_URL || 'http://druxt-auth.ddev.site'
33

44
export default {
55
// Global page headers: https://go.nuxtjs.dev/config-head
@@ -31,17 +31,19 @@ export default {
3131
components: true,
3232

3333
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
34-
buildModules: ['druxt-auth', 'druxt'],
34+
buildModules: [
35+
['druxt-auth', {
36+
clientId: process.env.OAUTH_CLIENT_ID,
37+
clientSecret: 'secret'
38+
}],
39+
'druxt'
40+
],
3541

3642
// Modules: https://go.nuxtjs.dev/config-modules
3743
// modules: [],
3844

3945
// DruxtJS: https://druxtjs.org
40-
druxt: {
41-
baseUrl,
42-
// Enable OAuth2 authentication.
43-
auth: { clientId: process.env.OAUTH_CLIENT_ID },
44-
},
46+
druxt: { baseUrl },
4547

4648
// Build Configuration: https://go.nuxtjs.dev/config-build
4749
build: {

example/nuxt/pages/index.vue

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
<div>
33
<template v-if="!this.$nuxt.$auth.loggedIn">
44
<h1>You are not logged in :(</h1>
5-
<button @click="$auth.loginWith('drupal-authorization_code')">Click here to log in</button>
6-
<pre><code>Username: admin
7-
Password: password</code></pre>
5+
<button @click="$auth.loginWith('drupal-authorization_code')">Click here to log in with Authorization grant</button>
6+
<details open style="border: 1px solid; padding: 0.5rem; margin-top: 1rem;">
7+
<summary>Login with Password grant</summary>
8+
<label>Username: <input v-model="username" /></label><br />
9+
<label>Password: <input v-model="password" /></label><br />
10+
<button @click="login">Log in</button>
11+
</details>
812
</template>
913

1014
<template v-else>
@@ -14,3 +18,22 @@ Password: password</code></pre>
1418
</template>
1519
</div>
1620
</template>
21+
22+
<script>
23+
export default {
24+
data: () => ({
25+
username: 'admin',
26+
password: 'password'
27+
}),
28+
methods: {
29+
login() {
30+
this.$auth.loginWith('drupal-password', {
31+
data: {
32+
username: this.username,
33+
password: this.password
34+
}
35+
})
36+
}
37+
}
38+
}
39+
</script>

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
"vue-jest": "3.0.7",
5151
"vue-template-compiler": "^2.6.14"
5252
},
53+
"peerDependencies": {
54+
"axios": "^0.27.2",
55+
"body-parser": "^1.20.0"
56+
},
5357
"publishConfig": {
5458
"access": "public"
5559
}

src/index.js

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1+
import axios from 'axios'
2+
import bodyParser from 'body-parser'
3+
14
// eslint-disable-next-line no-unused-vars
25
const NuxtModule = function (moduleOptions = {}) {
3-
const options = this.options.druxt || {}
6+
const options = {
7+
...this.options.druxt || {},
8+
auth: {
9+
...(this.options.druxt || {}).auth || {},
10+
clientId: undefined,
11+
clientSecret: undefined,
12+
...moduleOptions,
13+
}
14+
}
15+
16+
// Check if cliend ID is provided.
17+
if (!options.auth.clientId) {
18+
throw new Error('DruxtAuth requires a clientId to be provided.')
19+
}
20+
421
const { baseUrl } = options
522

623
// @nuxtjs/auth-next module settings.
@@ -28,10 +45,93 @@ const NuxtModule = function (moduleOptions = {}) {
2845
codeChallengeMethod: 'S256',
2946
},
3047

48+
// Password grant with API secret.
49+
'drupal-password': {
50+
scheme: 'refresh',
51+
token: {
52+
property: 'access_token',
53+
type: 'Bearer',
54+
name: 'Authorization',
55+
maxAge: 60 * 60 * 24 * 365
56+
},
57+
refreshToken: {
58+
property: 'refresh_token',
59+
data: 'refresh_token',
60+
maxAge: 60 * 60 * 24 * 30
61+
},
62+
endpoints: {
63+
token: baseUrl + '/oauth/token',
64+
login: {
65+
baseURL: '',
66+
url: '/_auth/drupal-password/token'
67+
},
68+
logout: false,
69+
refresh: {
70+
baseURL: '',
71+
url: '/_auth/drupal-password/token'
72+
},
73+
user: {
74+
url: baseUrl + '/oauth/userinfo',
75+
method: 'post'
76+
},
77+
},
78+
user: {
79+
property: false
80+
},
81+
grantType: 'password'
82+
},
83+
3184
...(this.options.auth || {}).strategies
3285
},
3386
}
3487

88+
// Add password grant server middleware.
89+
this.options.serverMiddleware.unshift({
90+
path: '/_auth/drupal-password/token',
91+
handler: async (req, res, next) => {
92+
if (req.method !== 'POST') {
93+
return next()
94+
}
95+
96+
const formMiddleware = bodyParser.json()
97+
await formMiddleware(req, res, async () => {
98+
const data = req.body
99+
100+
if (data.grant_type === 'password' && (!data.username || !data.password)) {
101+
return next(new Error('Invalid username or password'))
102+
}
103+
104+
try {
105+
// Build POST data string.
106+
const postData = new URLSearchParams({
107+
client_id: (options.auth || {}).clientId || process.env.DRUXT_AUTH_CLIENT_ID,
108+
client_secret: (options.auth || {}).clientSecret || process.env.DRUXT_AUTH_CLIENT_SECRET,
109+
...data
110+
}).toString()
111+
112+
// Request token,
113+
const response = await axios.post(
114+
this.options.auth.strategies['drupal-password'].endpoints.token,
115+
postData,
116+
{
117+
headers: {
118+
'Content-Type': 'application/x-www-form-urlencoded'
119+
},
120+
}
121+
)
122+
123+
// Return response data.
124+
res.end(JSON.stringify(response.data))
125+
} catch(err) {
126+
// Handle error.
127+
console.error(err)
128+
res.statusCode = (err.response || {}).statusCode || 500
129+
res.end(JSON.stringify({ ...((err.response || {}).data || {}) }))
130+
}
131+
})
132+
}
133+
})
134+
35135
// Enable Vuex Store.
36136
this.options.store = true
37137

test/__snapshots__/index.test.js.snap

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,51 @@ Object {
88
},
99
"strategies": Object {
1010
"drupal-authorization_code": Object {
11-
"clientId": undefined,
11+
"clientId": "mock-client-id",
1212
"codeChallengeMethod": "S256",
1313
"endpoints": Object {
14-
"authorization": "undefined/oauth/authorize",
15-
"token": "undefined/oauth/token",
16-
"userInfo": "undefined/oauth/userinfo",
14+
"authorization": "https://demo-api.druxtjs.org/oauth/authorize",
15+
"token": "https://demo-api.druxtjs.org/oauth/token",
16+
"userInfo": "https://demo-api.druxtjs.org/oauth/userinfo",
1717
},
1818
"grantType": "authorization_code",
1919
"responseType": "code",
2020
"scheme": "oauth2",
2121
},
22+
"drupal-password": Object {
23+
"endpoints": Object {
24+
"login": Object {
25+
"baseURL": "",
26+
"url": "/_auth/drupal-password/token",
27+
},
28+
"logout": false,
29+
"refresh": Object {
30+
"baseURL": "",
31+
"url": "/_auth/drupal-password/token",
32+
},
33+
"token": "https://demo-api.druxtjs.org/oauth/token",
34+
"user": Object {
35+
"method": "post",
36+
"url": "https://demo-api.druxtjs.org/oauth/userinfo",
37+
},
38+
},
39+
"grantType": "password",
40+
"refreshToken": Object {
41+
"data": "refresh_token",
42+
"maxAge": 2592000,
43+
"property": "refresh_token",
44+
},
45+
"scheme": "refresh",
46+
"token": Object {
47+
"maxAge": 31536000,
48+
"name": "Authorization",
49+
"property": "access_token",
50+
"type": "Bearer",
51+
},
52+
"user": Object {
53+
"property": false,
54+
},
55+
},
2256
},
2357
}
2458
`;

0 commit comments

Comments
 (0)