Skip to content

Commit c3b3d47

Browse files
authored
Add basic OAuth docs
As discussed here: accounts-js#875 (comment) Issues: accounts-js#875 and accounts-js#1031
1 parent 3f02186 commit c3b3d47

File tree

1 file changed

+259
-2
lines changed

1 file changed

+259
-2
lines changed

website/docs/strategies/oauth.md

Lines changed: 259 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,262 @@
11
---
2-
title: Oauth
2+
id: oauth
3+
title: OAuth
4+
sidebar_label: OAuth
35
---
46

5-
Coming soon ...
7+
[Github](https://github.com/accounts-js/accounts/tree/master/packages/oauth) |
8+
[npm](https://www.npmjs.com/package/@accounts/oauth)
9+
10+
The `@accounts/oauth` package provides a secure system for a OAuth based login strategy.
11+
12+
## Install
13+
14+
```
15+
# With yarn
16+
yarn add @accounts/oauth
17+
18+
# Or if you use npm
19+
npm install @accounts/oauth --save
20+
```
21+
22+
## Usage
23+
24+
This example is written in Typescript - remove any type definitons if you are using plain JS.
25+
26+
```javascript
27+
import { AccountsServer } from '@accounts/server'
28+
import { AccountsOauth } from '@accounts/oauth'
29+
30+
// We create a new OAuth instance (with at least one provider)
31+
const accountsOauth = new AccountsOauth({
32+
// ... OAuth providers
33+
})
34+
35+
// We pass the OAuth instance the AccountsServer service list
36+
const accountsServer = new AccountsServer(...config, {
37+
oauth: accountsOauth,
38+
});
39+
```
40+
41+
## Setting up a provider
42+
43+
In this example we are going to use **[Nextcloud](https://docs.nextcloud.com/server/19/admin_manual/configuration_server/oauth2.html)** as an OAuth [OpenID Connect](https://en.wikipedia.org/wiki/OpenID_Connect) Authorization Server, but it works the same way with any other provider.
44+
45+
There's an example repo for **Google OAuth** (React-based): [here](https://github.com/accounts-js/accounts/pull/961)
46+
47+
### Register your app as OAuth client with the provider
48+
49+
For Nextcloud, read [their docs](https://docs.nextcloud.com/server/19/admin_manual/configuration_server/oauth2.html) to set up your app as an OAuth Client. You'll need the details in the next step.
50+
51+
### Create the Login dialog UI
52+
53+
In the appropriate place of your app, place an "Authenticate via Nextcloud" button that will open a popup window for the user to authenticate via the OAuth provider.
54+
55+
When receiving this code, the client will send it to the AccountsJS-based server, which will verify it with the provider (Nextcloud) itself (we will define the serverside part later).
56+
57+
58+
```typescript
59+
import qs from 'qs' // https://www.npmjs.com/package/qs
60+
61+
function startNextcloudLogin () {
62+
//ui.loginLoading = true
63+
console.log(process.env)
64+
const config = {
65+
nextcloudOAuthURL: 'https://your-nextcloud.org/apps/oauth2/authorize'
66+
clientID: '...' // The ID of the client you registered with the provider
67+
redirectURL: 'http://localhost:8080/oauth-callback/nextcloud' // arbitrary URL in your app that you need to register a handler for (shown in a later step)
68+
}
69+
70+
const params = {
71+
response_type: 'code',
72+
client_id: config.clientID as string,
73+
redirect_uri: config.redirectURL as string,
74+
// (put here any extra params needed - e.g. for google: 'scope')
75+
}
76+
77+
// Create a BroadcastChannel for the popup window to return the auth code
78+
// see: https://mdn.io/BroadcastChannel
79+
const oauthLoginChannel = new BroadcastChannel('oauthLoginChannel')
80+
oauthLoginChannel.onmessage = async e => {
81+
const code = e.data as string
82+
try {
83+
// Send this code to the AccountsJS-based server
84+
await accountsClient.loginWithService('oauth', { provider: 'nextcloud', code })
85+
// the 'provider' is key you specify in AccountsOauth config
86+
console.log('User in LoginDialog success', user)
87+
user.value = await accountsClient.getUser()
88+
//ui.loginSuccess()
89+
} catch (e) {
90+
console.error('Failed to authenticate with received token', code, e)
91+
//ui.error = (e as Error).message
92+
}
93+
//ui.loginLoading = false
94+
}
95+
96+
// Open popup window with OAuth provider page
97+
const width = 600, height = 600
98+
const left = window.innerWidth / 2 - width / 2
99+
const top = window.innerHeight / 2 - height / 2
100+
window.open(
101+
`${config.nextcloudOAuthURL}?${qs.stringify(params)}`,
102+
'',
103+
`toolbar=no, location=no, directories=no, status=no, menubar=no,
104+
scrollbars=no, resizable=no, copyhistory=no, width=${width},
105+
height=${height}, top=${top}, left=${left}`,
106+
)
107+
}
108+
```
109+
110+
### Create a handler for callback URI
111+
112+
The OAuth provider will redirect to the specified `redirectUri` with a query string appended `?code=...` - as we're still inside the popup window, the handler we define below will take that code and send it via the BroadcastChannel (created when opening the popup window) back to the main window.
113+
114+
The handler `oauthLoginChannel.onmessage` will use that code to authenticate against your app's accountsjs-based server.
115+
116+
Register a route with your router. Example with vue-router:
117+
```typescript
118+
{ path: '/oauth-callback/:service', component: () => import('components/auth/OAuthCallback.vue') }
119+
```
120+
121+
Define the handler (example based on vue-router):
122+
123+
```typescript
124+
import qs from 'qs'
125+
126+
export default defineComponent({
127+
setup () {
128+
const { route } = useRouter()
129+
130+
const service = route.value.params.service
131+
console.log('service:', service)
132+
133+
onMounted(() => {
134+
const queryParams = qs.parse(window.location.search, { ignoreQueryPrefix: true })
135+
136+
const loginChannel = new BroadcastChannel('oauthLoginChannel')
137+
loginChannel.postMessage(queryParams.code) // send the code
138+
loginChannel.close()
139+
window.close()
140+
})
141+
142+
return { ...toRefs(data), service }
143+
}
144+
})
145+
```
146+
147+
### Create the provider definition
148+
149+
In the `oauthLoginChannel.onmessage` handler, we called:
150+
```typescript
151+
accountsClient.loginWithService('oauth', { provider: 'nextcloud', code })
152+
```
153+
154+
AccountsJS client will send that code to the server, where define a provider:
155+
156+
```typescript
157+
const accountsOauth = new AccountsOauth({
158+
nextcloud: new AccountsNextcloudProvider(),
159+
})
160+
```
161+
162+
The provider is defined like this:
163+
```typescript
164+
export class AccountsNextcloudProvider implements OAuthProvider {
165+
166+
/* This method is called when the user returns from the provider with an authorization code */
167+
async authenticate(params: any): Promise<OAuthUser> {
168+
// params.code is the auth code that nextcloud OAuth provides to the client
169+
// then LoginDialog sends the code here via accountsClient.loginWithService
170+
// it is used here to authenticate against nextcloud and to get the user info
171+
172+
// Ask Nextcloud server if the code is valid, and which user it authenticates
173+
const response = await axios.post(
174+
config.get('accounts.oauth.nextcloud.token-endpoint'), // see: https://docs.nextcloud.com/server/19/admin_manual/configuration_server/oauth2.html
175+
qs.stringify({
176+
grant_type: 'authorization_code',
177+
code: params.code,
178+
client_id: config.get('accounts.oauth.nextcloud.id'), // must be the one that the frontend used to authenticate
179+
client_secret: config.get('accounts.oauth.nextcloud.secret'), // The provider defines this when you register your app as an OAuth client
180+
}),
181+
{
182+
headers: {
183+
'Content-Type': 'application/x-www-form-urlencoded',
184+
},
185+
},
186+
)
187+
188+
const data = response.data
189+
const token: string = data.access_token
190+
const userID: string = data.user_id
191+
192+
// Optional - query Nextcloud for additional user info:
193+
194+
// const userinfoEndpoint: string = config.get('accounts.oauth.nextcloud.userinfo-endpoint')
195+
// const userProfileRes = await axios.get(
196+
// `${userinfoEndpoint}${userID}`,
197+
// {
198+
// headers: {
199+
// 'OCS-APIRequest': true, // https://github.com/nextcloud/server/issues/2753#issuecomment-267959121
200+
// Authorization: `Bearer ${token}`,
201+
// Accept: 'application/json',
202+
// },
203+
// },
204+
// )
205+
// const userMeta: Object = userProfileRes.data.ocs.data
206+
// const groups = _.get(userMeta, 'groups', [])
207+
// const isAdmin = !!groups.includes('admin')
208+
209+
// This data will be passed to the getRegistrationPayload below, and to createJwtPayload (see optional step later)
210+
return {
211+
id: userID,
212+
//data: userMeta, isAdmin, groups,
213+
}
214+
}
215+
216+
/* If your server doesn't know the user yet, this method will be called to get initial user info to be stored in the DB */
217+
async getRegistrationPayload(oauthUser: OAuthUser): Promise<any> {
218+
console.log('OAuth Registration payload for:', oauthUser)
219+
return {
220+
// This is nextcloud-specific - TODO: Adapt to your provider
221+
// username: oauthUser.data.id,
222+
// email: oauthUser.data.email,
223+
// displayName: oauthUser.data.displayname,
224+
}
225+
}
226+
}
227+
```
228+
229+
### Try it out :)
230+
231+
This should be enough for a basic OAuth setup to work.
232+
233+
234+
## Optional: Extend the JWT token
235+
236+
In order to add custom fields to the JWT you need to pass a validateNewUser function when you instantiate the `@accounts/password` package.
237+
238+
```javascript
239+
new AccountsServer<ExtendedUserType>(
240+
{
241+
createJwtPayload: async (data, user) => {
242+
// data is the object returned from AccountsNextcloudProvider.authenticate
243+
// user is the user fetched from the db
244+
245+
const nextcloudData = _.get(user.services, 'nextcloud')
246+
if (!nextcloudData) {
247+
console.log('Extending JWT skipped - no Nextcloud data') // seems to be called sometimes without the data
248+
return
249+
}
250+
251+
// return additional data for the JWT payload
252+
return {
253+
isAdmin: nextcloudData.isAdmin,
254+
groups: nextcloudData.groups,
255+
}
256+
},
257+
//... other server options
258+
},
259+
//... services config
260+
)
261+
```
262+

0 commit comments

Comments
 (0)