@@ -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 () {
9299export 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