@@ -36,6 +36,18 @@ export class PasskeyExtensionService {
3636 return btoa ( binary ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / g, '' ) ;
3737 }
3838
39+ private async readJsonBody < T > ( response : Response ) : Promise < T | undefined > {
40+ const contentType = response . headers . get ( 'content-type' ) ?? '' ;
41+ if ( ! contentType . toLowerCase ( ) . includes ( 'application/json' ) ) {
42+ return undefined ;
43+ }
44+ try {
45+ return ( await response . json ( ) ) as T ;
46+ } catch {
47+ return undefined ;
48+ }
49+ }
50+
3951 /**
4052 * Register a new passkey for the current user (must already be logged in).
4153 */
@@ -54,16 +66,13 @@ export class PasskeyExtensionService {
5466 const parsed = kc . tokenParsed as Record < string , unknown > | undefined ;
5567 const accountId = String ( parsed ?. [ 'sub' ] ?? parsed ?. [ 'preferred_username' ] ?? '' ) ;
5668 const accountName = String ( parsed ?. [ 'preferred_username' ] ?? parsed ?. [ 'email' ] ?? '' ) ;
57- const displayName = String (
58- parsed ?. [ 'name' ] ??
59- ( [ parsed ?. [ 'given_name' ] , parsed ?. [ 'family_name' ] ] . filter ( Boolean ) . join ( ' ' ) || accountName || 'User' )
60- ) ;
69+ const displayName = String ( parsed ?. [ 'name' ] ?? ( [ parsed ?. [ 'given_name' ] , parsed ?. [ 'family_name' ] ] . filter ( Boolean ) . join ( ' ' ) || accountName || 'User' ) ) ;
6170
6271 if ( ! accountId || ! accountName ) {
6372 throw new Error ( 'Missing user identity in token for passkey registration.' ) ;
6473 }
6574
66- const challengeRes = await fetch ( this . getUrl ( 'challenge' ) ) ;
75+ const challengeRes = await fetch ( this . getUrl ( 'challenge' ) , { credentials : 'include' } ) ;
6776 if ( ! challengeRes . ok ) {
6877 throw new Error ( `Failed to get WebAuthn challenge (${ challengeRes . status } )` ) ;
6978 }
@@ -94,11 +103,13 @@ export class PasskeyExtensionService {
94103 credentialId : this . bufferToBase64Url ( credential . rawId ) ,
95104 rawId : this . bufferToBase64Url ( credential . rawId ) ,
96105 clientDataJSON : this . bufferToBase64Url ( response . clientDataJSON ) ,
97- attestationObject : this . bufferToBase64Url ( response . attestationObject )
106+ attestationObject : this . bufferToBase64Url ( response . attestationObject ) ,
107+ challenge
98108 } ;
99109
100110 const saveRes = await fetch ( this . getUrl ( 'save' ) , {
101111 method : 'POST' ,
112+ credentials : 'include' ,
102113 headers : {
103114 'Content-Type' : 'application/json' ,
104115 Authorization : `Bearer ${ token } `
@@ -115,15 +126,17 @@ export class PasskeyExtensionService {
115126 }
116127
117128 /**
118- * Sign in with passkey only (no Keycloak UI redirect). Returns tokens from {@code POST /passkey/authenticate}.
129+ * Sign in with passkey only (no Keycloak UI redirect).
130+ * The extension endpoint sets the Keycloak login cookie; the SPA should then reload
131+ * and let keycloak-js initialize via check-sso.
119132 */
120- async signInWithPasskey ( ) : Promise < { access_token : string ; refresh_token : string } > {
121- const optionsResponse = await fetch ( this . getUrl ( 'get-credential-id' ) ) ;
122- const res = ( await optionsResponse . json ( ) ) as { challenge ?: string ; credentialId ?: string ; error ?: string } ;
133+ async signInWithPasskey ( ) : Promise < void > {
134+ const optionsResponse = await fetch ( this . getUrl ( 'challenge' ) , { credentials : 'include' } ) ;
135+ const res = await this . readJsonBody < { challenge ?: string ; credentialId ?: string ; error ?: string } > ( optionsResponse ) ;
123136 if ( ! optionsResponse . ok ) {
124137 throw new Error ( res ?. error || `Failed to get passkey options (${ optionsResponse . status } )` ) ;
125138 }
126- if ( ! res . challenge ) {
139+ if ( ! res ? .challenge ) {
127140 throw new Error ( 'Invalid challenge response from server' ) ;
128141 }
129142
@@ -132,9 +145,7 @@ export class PasskeyExtensionService {
132145 userVerification : 'preferred'
133146 } ;
134147 if ( res . credentialId ) {
135- publicKey . allowCredentials = [
136- { type : 'public-key' , id : this . base64UrlToUint8Array ( res . credentialId ) as BufferSource }
137- ] ;
148+ publicKey . allowCredentials = [ { type : 'public-key' , id : this . base64UrlToUint8Array ( res . credentialId ) as BufferSource } ] ;
138149 }
139150
140151 const credential = ( await navigator . credentials . get ( { publicKey } ) ) as PublicKeyCredential | null ;
@@ -154,17 +165,19 @@ export class PasskeyExtensionService {
154165
155166 const authRes = await fetch ( this . getUrl ( 'authenticate' ) , {
156167 method : 'POST' ,
168+ credentials : 'include' ,
169+ redirect : 'manual' ,
157170 headers : { 'Content-Type' : 'application/json' } ,
158171 body : JSON . stringify ( payload )
159172 } ) ;
160173
161- const authResult = ( await authRes . json ( ) ) as { access_token ?: string ; refresh_token ?: string ; error ?: string } ;
174+ if ( authRes . type === 'opaqueredirect' ) {
175+ return ;
176+ }
177+
178+ const authResult = await this . readJsonBody < { error ?: string } > ( authRes ) ;
162179 if ( ! authRes . ok ) {
163180 throw new Error ( authResult ?. error || `Passkey authentication failed (${ authRes . status } )` ) ;
164181 }
165- if ( ! authResult . access_token || ! authResult . refresh_token ) {
166- throw new Error ( 'Invalid token response from server' ) ;
167- }
168- return { access_token : authResult . access_token , refresh_token : authResult . refresh_token } ;
169182 }
170183}
0 commit comments