@@ -28,14 +28,15 @@ import * as QueryString from 'query-string';
2828import { FormField , FormStructure } from 'app/components' ;
2929import { HydraInvitationForm } from 'app/containers/admin/hydra-invitation-form' ;
3030import { HydraButtons , RejectHydraSignup } from 'app/containers/auth/hydra-buttons' ;
31- import { AUTH_CLIENT_ID , AUTH_URI } from 'app/containers/constants' ;
31+ import { AUTH_CLIENT_ID , AUTH_URI , OAUTH_PROVIDER } from 'app/containers/constants' ;
3232
3333import { CallbackArgs , getSignupArgs , getLoginArgs } from './callback-url' ;
3434
3535export const PasswordError = new Error ( 'Kratos identity server error: Password method not found in flows.' ) ;
3636export const FlowIDError = new Error ( 'Auth server requires a flow parameter in the query string, but none were found.' ) ;
3737
38- const kratosClient = V0alpha2ApiFactory ( null , '/oauth/kratos' ) ;
38+ const kratosUri = `${ location . protocol } //${ location . host } /oauth/kratos` ;
39+ const kratosClient = V0alpha2ApiFactory ( null , kratosUri ) ;
3940
4041// Renders a form with an error and no fields.
4142const displayErrorFormStructure = ( error : Error ) : FormStructure => ( {
@@ -81,56 +82,190 @@ function nodesToFormFields(nodes: Array<UiNode>): Array<FormField> {
8182 . filter ( node => node ) ;
8283}
8384
85+ // Dynamic client registration types
86+ interface OAuth2Client {
87+ client_id : string ;
88+ client_secret ?: string ;
89+ client_name ?: string ;
90+ redirect_uris ?: string [ ] ;
91+ grant_types ?: string [ ] ;
92+ response_types ?: string [ ] ;
93+ scope ?: string ;
94+ token_endpoint_auth_method ?: string ;
95+ }
96+
97+ interface DynamicClientRegistrationResponse extends OAuth2Client {
98+ client_id_issued_at ?: number ;
99+ client_secret_expires_at ?: number ;
100+ registration_client_uri ?: string ;
101+ registration_access_token ?: string ;
102+ }
103+
104+ // Cache for the dynamically registered client ID
105+ let cachedClientId : string | null = null ;
106+
107+ async function getDynamicClientId ( ) : Promise < string > {
108+ // Only use dynamic registration for hydra provider
109+ if ( OAUTH_PROVIDER !== 'hydra' ) {
110+ return AUTH_CLIENT_ID ;
111+ }
112+
113+ // Return cached client ID if available
114+ if ( cachedClientId ) {
115+ return cachedClientId ;
116+ }
117+
118+ // Use /oauth/hydra prefix which will be stripped by the proxy
119+ // Preserve the current port (e.g., :8080 for dev server)
120+ const registrationEndpoint = `${ window . location . protocol } //${ window . location . host } /oauth/hydra/oauth2/register` ;
121+ console . log ( `Dynamic client registration endpoint: ${ registrationEndpoint } ` ) ;
122+
123+ const redirect_uri = `${ window . location . protocol } //${ window . location . host } /auth/callback` ;
124+ console . log ( `Using redirect URI: ${ redirect_uri } ` ) ;
125+
126+ const clientMetadata : Partial < OAuth2Client > = {
127+ client_name : 'pixie-auth-client' ,
128+ redirect_uris : [
129+ redirect_uri ,
130+ ] ,
131+ grant_types : [ 'authorization_code' , 'refresh_token' , 'implicit' ] ,
132+ response_types : [ 'code' , 'id_token' , 'token' ] ,
133+ scope : 'openid offline notifications gist vizier' ,
134+ token_endpoint_auth_method : 'none' ,
135+ } ;
136+
137+ try {
138+ const response = await fetch ( registrationEndpoint , {
139+ method : 'POST' ,
140+ headers : {
141+ 'Content-Type' : 'application/json' ,
142+ 'Accept' : 'application/json' ,
143+ } ,
144+ body : JSON . stringify ( clientMetadata ) ,
145+ } ) ;
146+
147+ if ( ! response . ok ) {
148+ const errorText = await response . text ( ) ;
149+ throw new Error ( `Dynamic client registration failed: ${ response . status } ${ response . statusText } - ${ errorText } ` ) ;
150+ }
151+
152+ const data : DynamicClientRegistrationResponse = await response . json ( ) ;
153+
154+ if ( ! data . client_id ) {
155+ throw new Error ( 'No client_id returned from dynamic registration' ) ;
156+ }
157+
158+ // Cache the client ID for future use
159+ cachedClientId = data . client_id ;
160+
161+ console . log ( 'Successfully registered dynamic OAuth2 client:' , data . client_id ) ;
162+ return data . client_id ;
163+ } catch ( error ) {
164+ console . error ( 'Failed to register dynamic client:' , error ) ;
165+ // Fall back to static client ID if dynamic registration fails
166+ console . warn ( 'Falling back to static client ID' ) ;
167+ return AUTH_CLIENT_ID ;
168+ }
169+ }
170+
84171export const HydraClient = {
85- userManager : new UserManager ( {
86- authority : AUTH_URI ,
87- client_id : AUTH_CLIENT_ID ,
88- redirect_uri : `${ window . location . origin } /auth/callback` ,
89- loadUserInfo : false ,
90- scope : 'vizier' ,
91- response_type : 'token' ,
92- } ) ,
172+ userManager : null as UserManager | null ,
173+ userManagerPromise : null as Promise < UserManager > | null ,
174+
175+ async getUserManager ( ) : Promise < UserManager > {
176+ // Return existing manager if available
177+ if ( this . userManager ) {
178+ return this . userManager ;
179+ }
180+
181+ // Return existing promise if initialization is in progress
182+ if ( this . userManagerPromise ) {
183+ return this . userManagerPromise ;
184+ }
185+
186+ // Start initialization
187+ this . userManagerPromise = ( async ( ) => {
188+ const clientId = await getDynamicClientId ( ) ;
189+
190+ this . userManager = new UserManager ( {
191+ authority : AUTH_URI ,
192+ client_id : clientId ,
193+ redirect_uri : `${ window . location . origin } /auth/callback` ,
194+ loadUserInfo : false ,
195+ scope : 'vizier' ,
196+ response_type : 'token' ,
197+ } ) ;
198+
199+ return this . userManager ;
200+ } ) ( ) ;
201+
202+ return this . userManagerPromise ;
203+ } ,
93204
94205 loginRequest ( ) : void {
95- this . userManager . signinRedirect ( {
96- prompt : 'login' ,
97- state : {
98- redirectArgs : getLoginArgs ( ) ,
99- } ,
206+ // Fire and forget - the redirect will happen once the client ID is obtained
207+ this . getUserManager ( ) . then ( userManager => {
208+ userManager . signinRedirect ( {
209+ prompt : 'login' ,
210+ state : {
211+ redirectArgs : getLoginArgs ( ) ,
212+ } ,
213+ } ) ;
214+ } ) . catch ( error => {
215+ console . error ( 'Failed to initiate login:' , error ) ;
216+ // Could potentially show an error to the user here
100217 } ) ;
101218 } ,
102219
103220 signupRequest ( ) : void {
104- this . userManager . signinRedirect ( {
105- prompt : 'login' ,
106- state : {
107- redirectArgs : getSignupArgs ( ) ,
108- } ,
221+ // Fire and forget - the redirect will happen once the client ID is obtained
222+ this . getUserManager ( ) . then ( userManager => {
223+ userManager . signinRedirect ( {
224+ prompt : 'login' ,
225+ state : {
226+ redirectArgs : getSignupArgs ( ) ,
227+ } ,
228+ } ) ;
229+ } ) . catch ( error => {
230+ console . error ( 'Failed to initiate signup:' , error ) ;
231+ // Could potentially show an error to the user here
109232 } ) ;
110233 } ,
111234
112235 refetchToken ( ) : void {
113- this . userManager . signinRedirect ( {
114- state : {
115- redirectArgs : getLoginArgs ( ) ,
116- } ,
236+ // Fire and forget - the redirect will happen once the client ID is obtained
237+ this . getUserManager ( ) . then ( userManager => {
238+ userManager . signinRedirect ( {
239+ state : {
240+ redirectArgs : getLoginArgs ( ) ,
241+ } ,
242+ } ) ;
243+ } ) . catch ( error => {
244+ console . error ( 'Failed to refetch token:' , error ) ;
245+ // Could potentially show an error to the user here
117246 } ) ;
118247 } ,
119248
120249 handleToken ( ) : Promise < CallbackArgs > {
121- return new Promise < CallbackArgs > ( ( resolve , reject ) => {
122- this . userManager . signinRedirectCallback ( )
123- . then ( ( user ) => {
124- if ( ! user ) {
125- reject ( new Error ( 'user is undefined, please try logging in again' ) ) ;
126- }
127- resolve ( {
128- redirectArgs : user . state . redirectArgs ,
129- token : {
130- accessToken : user . access_token ,
131- } ,
132- } ) ;
133- } ) . catch ( reject ) ;
250+ return new Promise < CallbackArgs > ( async ( resolve , reject ) => {
251+ try {
252+ const userManager = await this . getUserManager ( ) ;
253+ const user = await userManager . signinRedirectCallback ( ) ;
254+
255+ if ( ! user ) {
256+ reject ( new Error ( 'user is undefined, please try logging in again' ) ) ;
257+ return ;
258+ }
259+
260+ resolve ( {
261+ redirectArgs : user . state . redirectArgs ,
262+ token : {
263+ accessToken : user . access_token ,
264+ } ,
265+ } ) ;
266+ } catch ( error ) {
267+ reject ( error ) ;
268+ }
134269 } ) ;
135270 } ,
136271
0 commit comments