Skip to content

Commit aa606f6

Browse files
Add OAuth implicit grant (response_type=token) for remoteStorage
The remoteStorage.js client library uses the implicit flow (RFC 6749 §4.2) — it passes response_type=token and expects the access token directly in the redirect URL fragment. Also allows unregistered clients for implicit flow, since RS clients use their origin URL as client_id without pre-registration.
1 parent 127b6ba commit aa606f6

1 file changed

Lines changed: 38 additions & 11 deletions

File tree

src/ap/routes/oauth.js

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,19 @@ function parseBody (request) {
4646
* Validate client_id and redirect_uri against registered client
4747
* Returns { client, error } — client is null if validation fails
4848
*/
49-
function validateClient (clientId, redirectUri) {
49+
function validateClient (clientId, redirectUri, responseType) {
5050
if (!clientId || !redirectUri) {
5151
return { client: null, error: 'Missing client_id or redirect_uri' }
5252
}
5353

5454
const client = getClient(clientId)
55+
56+
// Implicit flow (response_type=token) allows unregistered clients
57+
// remoteStorage clients pass their origin URL as client_id without pre-registration
58+
if (!client && responseType === 'token') {
59+
return { client: { name: clientId, redirect_uri: redirectUri }, error: null }
60+
}
61+
5562
if (!client) {
5663
return { client: null, error: 'Unknown client_id. Register via POST /api/v1/apps first.' }
5764
}
@@ -71,17 +78,17 @@ export function createAuthorizeHandler () {
7178
return async (request, reply) => {
7279
const { client_id, redirect_uri, response_type, scope, state } = request.query
7380

74-
if (response_type && response_type !== 'code') {
75-
return reply.code(400).send({ error: 'unsupported_response_type', error_description: 'Only response_type=code is supported' })
81+
if (response_type && response_type !== 'code' && response_type !== 'token') {
82+
return reply.code(400).send({ error: 'unsupported_response_type', error_description: 'Supported: code, token' })
7683
}
7784

78-
const { client, error } = validateClient(client_id, redirect_uri)
85+
const { client, error } = validateClient(client_id, redirect_uri, response_type)
7986
if (!client) {
8087
return reply.code(400).send({ error: 'invalid_client', error_description: error })
8188
}
8289

8390
return reply.type('text/html').send(
84-
loginPage({ clientId: client_id, redirectUri: redirect_uri, scope: scope || 'read', state, clientName: client.name })
91+
loginPage({ clientId: client_id, redirectUri: redirect_uri, responseType: response_type || 'code', scope: scope || 'read', state, clientName: client.name })
8592
)
8693
}
8794
}
@@ -92,28 +99,47 @@ export function createAuthorizeHandler () {
9299
export function createAuthorizePostHandler () {
93100
return async (request, reply) => {
94101
const body = parseBody(request)
95-
const { username, password, client_id, redirect_uri, scope, state } = body
102+
const { username, password, client_id, redirect_uri, response_type, scope, state } = body
96103

97104
// Validate client + redirect_uri (prevent open redirect via form tampering)
98-
const { client, error: clientError } = validateClient(client_id, redirect_uri)
105+
const { client, error: clientError } = validateClient(client_id, redirect_uri, response_type)
99106
if (!client) {
100107
return reply.code(400).send({ error: 'invalid_client', error_description: clientError })
101108
}
102109

103110
if (!username || !password) {
104111
return reply.type('text/html').send(
105-
loginPage({ clientId: client_id, redirectUri: redirect_uri, scope, state, clientName: client.name, error: 'Username and password are required' })
112+
loginPage({ clientId: client_id, redirectUri: redirect_uri, responseType: response_type || 'code', scope, state, clientName: client.name, error: 'Username and password are required' })
106113
)
107114
}
108115

109116
const account = await authenticate(username, password)
110117
if (!account) {
111118
return reply.type('text/html').send(
112-
loginPage({ clientId: client_id, redirectUri: redirect_uri, scope, state, clientName: client.name, error: 'Invalid username or password' })
119+
loginPage({ clientId: client_id, redirectUri: redirect_uri, responseType: response_type || 'code', scope, state, clientName: client.name, error: 'Invalid username or password' })
113120
)
114121
}
115122

116-
// Generate one-time auth code (10 min TTL)
123+
// Implicit grant (response_type=token) — return token directly in fragment (RFC 6749 §4.2.2)
124+
// Used by remoteStorage clients
125+
if (response_type === 'token') {
126+
const accessToken = createToken(account.webId)
127+
128+
// Handle OOB — display token
129+
if (redirect_uri === OOB_REDIRECT) {
130+
return reply.type('text/html').send(oobPage(accessToken))
131+
}
132+
133+
// Fragment-based redirect (token MUST be in fragment, not query — RFC 6749 §4.2.2)
134+
const params = new URLSearchParams()
135+
params.set('access_token', accessToken)
136+
params.set('token_type', 'bearer')
137+
params.set('scope', scope || 'read')
138+
if (state) params.set('state', state)
139+
return reply.redirect(`${redirect_uri}#${params.toString()}`)
140+
}
141+
142+
// Authorization code grant (response_type=code) — generate one-time auth code (10 min TTL)
117143
const code = crypto.randomUUID()
118144
authCodes.set(code, {
119145
clientId: client_id,
@@ -196,7 +222,7 @@ export function createTokenHandler () {
196222
/**
197223
* Minimal login page HTML
198224
*/
199-
function loginPage ({ clientId, redirectUri, scope, state, clientName, error }) {
225+
function loginPage ({ clientId, redirectUri, responseType, scope, state, clientName, error }) {
200226
const escapedError = error ? escapeHtml(error) : ''
201227
const escapedName = escapeHtml(clientName || clientId || 'Unknown app')
202228

@@ -230,6 +256,7 @@ function loginPage ({ clientId, redirectUri, scope, state, clientName, error })
230256
<form method="POST" action="/oauth/authorize">
231257
<input type="hidden" name="client_id" value="${escapeHtml(clientId || '')}">
232258
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri || '')}">
259+
<input type="hidden" name="response_type" value="${escapeHtml(responseType || 'code')}">
233260
<input type="hidden" name="scope" value="${escapeHtml(scope || 'read')}">
234261
${state ? `<input type="hidden" name="state" value="${escapeHtml(state)}">` : ''}
235262
<label for="username">Username</label>

0 commit comments

Comments
 (0)