55
66import * as jose from 'jose' ;
77import crypto from 'crypto' ;
8- import { authenticate , findByEmail } from './accounts.js' ;
8+ import { authenticate } from './accounts.js' ;
99import { getJwks } from './keys.js' ;
10- import { createToken as createSimpleToken } from '../auth/token.js' ;
1110
1211/**
1312 * Handle POST /idp/credentials
14- * Accepts email/password and returns access token
13+ * Accepts email/password (or username/password) and returns access token
1514 *
1615 * Request body (JSON or form):
17- * - email: User email
16+ * - email or username : User email address
1817 * - password: User password
1918 *
2019 * Optional headers:
@@ -47,22 +46,22 @@ export async function handleCredentials(request, reply, issuer) {
4746 // Not valid JSON
4847 }
4948 }
50- email = body ?. email ;
49+ email = body ?. email || body ?. username ;
5150 password = body ?. password ;
5251 } else if ( contentType . includes ( 'application/x-www-form-urlencoded' ) ) {
5352 // Parse form-encoded body
5453 if ( typeof body === 'string' ) {
5554 const params = new URLSearchParams ( body ) ;
56- email = params . get ( 'email' ) ;
55+ email = params . get ( 'email' ) || params . get ( 'username' ) ;
5756 password = params . get ( 'password' ) ;
5857 } else if ( typeof body === 'object' ) {
59- email = body ?. email ;
58+ email = body ?. email || body ?. username ;
6059 password = body ?. password ;
6160 }
6261 } else {
6362 // Try to parse as object
6463 if ( typeof body === 'object' ) {
65- email = body ?. email ;
64+ email = body ?. email || body ?. username ;
6665 password = body ?. password ;
6766 }
6867 }
@@ -71,7 +70,7 @@ export async function handleCredentials(request, reply, issuer) {
7170 if ( ! email || ! password ) {
7271 return reply . code ( 400 ) . send ( {
7372 error : 'invalid_request' ,
74- error_description : 'Email and password are required' ,
73+ error_description : 'Username/email and password are required' ,
7574 } ) ;
7675 }
7776
@@ -92,7 +91,8 @@ export async function handleCredentials(request, reply, issuer) {
9291 if ( dpopHeader ) {
9392 try {
9493 // Validate DPoP proof and extract thumbprint
95- dpopJkt = await validateDpopProof ( dpopHeader , 'POST' , `${ issuer } /idp/credentials` ) ;
94+ const credUrl = `${ issuer . replace ( / \/ $ / , '' ) } /idp/credentials` ;
95+ dpopJkt = await validateDpopProof ( dpopHeader , 'POST' , credUrl ) ;
9696 } catch ( err ) {
9797 return reply . code ( 400 ) . send ( {
9898 error : 'invalid_dpop_proof' ,
@@ -102,39 +102,38 @@ export async function handleCredentials(request, reply, issuer) {
102102 }
103103
104104 const expiresIn = 3600 ; // 1 hour
105- let accessToken ;
106- let tokenType ;
107105
106+ // Always generate a proper JWT - CTH requires JWT format
107+ const jwks = await getJwks ( ) ;
108+ const signingKey = jwks . keys [ 0 ] ;
109+ const privateKey = await jose . importJWK ( signingKey , 'ES256' ) ;
110+
111+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
112+ const tokenPayload = {
113+ iss : issuer ,
114+ sub : account . id ,
115+ aud : 'solid' , // Solid-OIDC requires this audience
116+ webid : account . webId ,
117+ iat : now ,
118+ exp : now + expiresIn ,
119+ jti : crypto . randomUUID ( ) ,
120+ client_id : 'credentials_client' ,
121+ scope : 'openid webid' ,
122+ } ;
123+
124+ // Add DPoP binding confirmation if DPoP proof was provided
125+ let tokenType ;
108126 if ( dpopJkt ) {
109- // Generate DPoP-bound JWT for Solid-OIDC clients
110- const jwks = await getJwks ( ) ;
111- const signingKey = jwks . keys [ 0 ] ;
112- const privateKey = await jose . importJWK ( signingKey , 'ES256' ) ;
113-
114- const now = Math . floor ( Date . now ( ) / 1000 ) ;
115- const tokenPayload = {
116- iss : issuer ,
117- sub : account . id ,
118- aud : 'solid' ,
119- webid : account . webId ,
120- iat : now ,
121- exp : now + expiresIn ,
122- jti : crypto . randomUUID ( ) ,
123- client_id : 'credentials_client' ,
124- scope : 'openid webid' ,
125- cnf : { jkt : dpopJkt } ,
126- } ;
127-
128- accessToken = await new jose . SignJWT ( tokenPayload )
129- . setProtectedHeader ( { alg : 'ES256' , kid : signingKey . kid } )
130- . sign ( privateKey ) ;
127+ tokenPayload . cnf = { jkt : dpopJkt } ;
131128 tokenType = 'DPoP' ;
132129 } else {
133- // Generate simple token for Bearer auth (development/testing)
134- accessToken = createSimpleToken ( account . webId , expiresIn ) ;
135130 tokenType = 'Bearer' ;
136131 }
137132
133+ const accessToken = await new jose . SignJWT ( tokenPayload )
134+ . setProtectedHeader ( { alg : 'ES256' , kid : signingKey . kid } )
135+ . sign ( privateKey ) ;
136+
138137 // Response
139138 const response = {
140139 access_token : accessToken ,
@@ -206,10 +205,11 @@ export function handleCredentialsInfo(request, reply, issuer) {
206205 return {
207206 endpoint : `${ issuer } /idp/credentials` ,
208207 method : 'POST' ,
209- description : 'Obtain access tokens using email and password' ,
208+ description : 'Obtain access tokens using email/username and password' ,
210209 content_types : [ 'application/json' , 'application/x-www-form-urlencoded' ] ,
211210 parameters : {
212- email : 'User email address' ,
211+ email : 'User email address (or use "username")' ,
212+ username : 'Alias for email (for CTH compatibility)' ,
213213 password : 'User password' ,
214214 } ,
215215 optional_headers : {
0 commit comments