diff --git a/config/config.go b/config/config.go index 779a2ab4..6dab9a8b 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "encoding/json" + "strings" "github.com/fiware/VCVerifier/logging" ) @@ -107,6 +108,8 @@ type Server struct { TemplateDir string `mapstructure:"templateDir" default:"views/"` // directory of static files to be provided, f.e. to be used inside the templates StaticDir string `mapstructure:"staticDir" default:"views/static/"` + // PathPrefix is prepended to all routes and static assets except /health and /metrics + PathPrefix string `mapstructure:"pathPrefix" default:""` // ReadTimeout is the maximum duration for reading the entire request, including the body. ReadTimeout int `mapstructure:"readTimeout" default:"5"` @@ -118,6 +121,23 @@ type Server struct { ShutdownTimeout int `mapstructure:"shutdownTimeout" default:"5"` } +// NormalizedPathPrefix returns PathPrefix in a form safe to use both as a +// gin.RouterGroup prefix and as a string-concatenation prefix for +// server-built URLs. Unset ("") and an explicit "/" both mean "no prefix" +// and normalize to "", the identity value for both use sites. Any other +// value is trimmed of a trailing slash and given a leading slash if missing. +func (s Server) NormalizedPathPrefix() string { + prefix := strings.TrimSpace(s.PathPrefix) + prefix = strings.TrimSuffix(prefix, "/") + if prefix == "" { + return "" + } + if !strings.HasPrefix(prefix, "/") { + prefix = "/" + prefix + } + return prefix +} + // configuration for M2M interaction type M2M struct { // auth enabled for M2M interactions diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..da423a3a --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,27 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestServer_NormalizedPathPrefix(t *testing.T) { + tests := []struct { + name string + prefix string + want string + }{ + {"unset", "", ""}, + {"root slash", "/", ""}, + {"no leading slash", "myservice", "/myservice"}, + {"leading and trailing slash", "/myservice/", "/myservice"}, + {"trailing slash only", "myservice/", "/myservice"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Server{PathPrefix: tt.prefix} + assert.Equal(t, tt.want, s.NormalizedPathPrefix()) + }) + } +} diff --git a/config/provider.go b/config/provider.go index c00dff73..3d608af2 100644 --- a/config/provider.go +++ b/config/provider.go @@ -1,9 +1,6 @@ package config import ( - "fmt" - "os" - "github.com/gookit/config/v2" "github.com/gookit/config/v2/yaml" "github.com/mitchellh/mapstructure" @@ -28,8 +25,6 @@ func ReadConfig(configFile string) (configuration Configuration, err error) { } }) config.AddDriver(yaml.Driver) - usuario := os.Getenv("DB_USER") - fmt.Println("Usuario:", usuario) if err = config.LoadFiles(configFile); err != nil { return diff --git a/main.go b/main.go index b04a8ddc..113a7a45 100644 --- a/main.go +++ b/main.go @@ -113,9 +113,10 @@ func main() { } } - router := getRouter() + pathPrefix := configuration.Server.NormalizedPathPrefix() + router := getRouter(pathPrefix) - // health check + // health check - stays unprefixed regardless of pathPrefix router.GET("/health", HealthReq) allowedOrigins := ResolveAllowedOrigins(configuration.ConfigRepo.Services) @@ -131,7 +132,7 @@ func main() { //new template engine router.HTMLRender = ginview.Default() // static files for the frontend - router.Static("/static", configuration.Server.StaticDir) + router.Group(pathPrefix).Static("/static", configuration.Server.StaticDir) templateDir := configuration.Server.TemplateDir if templateDir != "" { @@ -271,7 +272,7 @@ func getConfigRouter(db *sql.DB, repo database.ServiceRepository) *gin.Engine { } // initiate the router -func getRouter() *gin.Engine { +func getRouter(pathPrefix string) *gin.Engine { // the openapi generated router uses the defaults, which we want to override to improve and configure logging writer := logging.GetGinInternalWriter() @@ -281,8 +282,9 @@ func getRouter() *gin.Engine { router.Use(logging.GinHandlerFunc(), gin.Recovery()) + group := router.Group(pathPrefix) for _, route := range api.NewRouter().Routes() { - router.Handle(route.Method, route.Path, route.HandlerFunc) + group.Handle(route.Method, route.Path, route.HandlerFunc) } return router diff --git a/main_test.go b/main_test.go index e8fc99a9..da51b7ae 100644 --- a/main_test.go +++ b/main_test.go @@ -505,6 +505,47 @@ func TestResolveAllowedOrigins(t *testing.T) { } } +func TestGetRouter_MountsRoutesUnderPrefix(t *testing.T) { + router := getRouter("/myservice") + + routes := router.Routes() + routeMap := make(map[string]bool) + for _, r := range routes { + routeMap[r.Method+" "+r.Path] = true + } + + assert.True(t, routeMap["GET /myservice/.well-known/jwks"], "expected route to be registered under the prefix") + assert.False(t, routeMap["GET /.well-known/jwks"], "unprefixed route should not exist once a prefix is configured") +} + +func TestGetRouter_NoPrefixMatchesTodayBehavior(t *testing.T) { + router := getRouter("") + + routes := router.Routes() + routeMap := make(map[string]bool) + for _, r := range routes { + routeMap[r.Method+" "+r.Path] = true + } + + assert.True(t, routeMap["GET /.well-known/jwks"], "with no prefix configured, routes should be registered at their bare paths") +} + +func TestGetRouter_HealthAndMetricsStayUnprefixed(t *testing.T) { + router := getRouter("/myservice") + // registered the same way main() does: directly on the raw engine, not the prefixed group + router.GET("/health", HealthReq) + + req := httptest.NewRequest(http.MethodGet, "/health", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code, "/health must stay reachable at root") + + req = httptest.NewRequest(http.MethodGet, "/myservice/health", nil) + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + assert.Equal(t, http.StatusNotFound, w.Code, "/health must not additionally be available under the prefix") +} + // Ensure init() sets gin to test mode without interfering with other tests func init() { gin.SetMode(gin.TestMode) diff --git a/openapi/api_api.go b/openapi/api_api.go index 925f0528..a2d9b667 100644 --- a/openapi/api_api.go +++ b/openapi/api_api.go @@ -238,14 +238,14 @@ func AuthorizationEndpoint(c *gin.Context) { return } case FRONTEND_V2: - redirect = buildFrontendV2Address(protocol, c.Request.Host, state, clientId, redirectUri, scope, nonce) + redirect = buildFrontendV2Address(protocol, c.Request.Host, getApiVerifier().GetPathPrefix(), state, clientId, redirectUri, scope, nonce) } c.Redirect(http.StatusFound, redirect) } -func buildFrontendV2Address(protocol, host, state, clientId, redirectUri, scope, nonce string) string { - logging.Log().Debugf("%s://%s/api/v2/loginQR?state=%s&client_id=%s&redirect_uri=%s&scope=%s&nonce=%s&request_mode=byReference", protocol, host, state, clientId, redirectUri, scope, nonce) - return fmt.Sprintf("%s://%s/api/v2/loginQR?state=%s&client_id=%s&redirect_uri=%s&scope=%s&nonce=%s&request_mode=byReference", protocol, host, state, clientId, redirectUri, scope, nonce) +func buildFrontendV2Address(protocol, host, prefix, state, clientId, redirectUri, scope, nonce string) string { + logging.Log().Debugf("%s://%s%s/api/v2/loginQR?state=%s&client_id=%s&redirect_uri=%s&scope=%s&nonce=%s&request_mode=byReference", protocol, host, prefix, state, clientId, redirectUri, scope, nonce) + return fmt.Sprintf("%s://%s%s/api/v2/loginQR?state=%s&client_id=%s&redirect_uri=%s&scope=%s&nonce=%s&request_mode=byReference", protocol, host, prefix, state, clientId, redirectUri, scope, nonce) } // GetToken - Token endpoint to exchange the authorization code with the actual JWT. diff --git a/openapi/api_api_test.go b/openapi/api_api_test.go index 74c158be..bf317edd 100644 --- a/openapi/api_api_test.go +++ b/openapi/api_api_test.go @@ -52,6 +52,7 @@ type mockVerifier struct { mockExchangeExpiration int64 mockExchangeRefresh string mockExchangeError error + mockPathPrefix string } func (mV *mockVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string, nonce string, requestType string) (qr string, err error) { @@ -90,6 +91,10 @@ func (mV *mockVerifier) GetHost() string { return "" } +func (mV *mockVerifier) GetPathPrefix() string { + return mV.mockPathPrefix +} + // TODO func (mV *mockVerifier) GetRequestObject(state string) (jwt string, err error) { return jwt, err @@ -689,6 +694,25 @@ func TestGetPresentationFromQuery(t *testing.T) { } } +func TestBuildFrontendV2Address(t *testing.T) { + tests := []struct { + testName string + prefix string + want string + }{ + {testName: "No prefix configured, path is unchanged", prefix: "", want: "https://verifier.org/api/v2/loginQR?state=my-state&client_id=my-client&redirect_uri=https://wallet.example/callback&scope=&nonce=my-nonce&request_mode=byReference"}, + {testName: "Prefix is inserted before the loginQR path", prefix: "/myservice", want: "https://verifier.org/myservice/api/v2/loginQR?state=my-state&client_id=my-client&redirect_uri=https://wallet.example/callback&scope=&nonce=my-nonce&request_mode=byReference"}, + } + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + actual := buildFrontendV2Address("https", "verifier.org", tc.prefix, "my-state", "my-client", "https://wallet.example/callback", "", "my-nonce") + if actual != tc.want { + t.Errorf("%s - Expected %s but was %s", tc.testName, tc.want, actual) + } + }) + } +} + func getNoHolderVPToken() string { return "ewogICJAY29udGV4dCI6IFsKICAgICJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIKICBdLAogICJ0eXBlIjogWwogICAgIlZlcmlmaWFibGVQcmVzZW50YXRpb24iCiAgXSwKICAidmVyaWZpYWJsZUNyZWRlbnRpYWwiOiBbCiAgICB7CiAgICAgICJ0eXBlcyI6IFsKICAgICAgICAiUGFja2V0RGVsaXZlcnlTZXJ2aWNlIiwKICAgICAgICAiVmVyaWZpYWJsZUNyZWRlbnRpYWwiCiAgICAgIF0sCiAgICAgICJAY29udGV4dCI6IFsKICAgICAgICAiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLAogICAgICAgICJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3N1aXRlcy9qd3MtMjAyMC92MSIKICAgICAgXSwKICAgICAgImNyZWRlbnRpYWxzU3ViamVjdCI6IHt9LAogICAgICAiYWRkaXRpb25hbFByb3AxIjoge30KICAgIH0KICBdLAogICJpZCI6ICJlYmM2ZjFjMiIsCiAgImhvbGRlciI6IHsKICAgICJub3RhIjogImhvbGRlciIKICB9LAogICJwcm9vZiI6IHsKICAgICJ0eXBlIjogIkpzb25XZWJTaWduYXR1cmUyMDIwIiwKICAgICJjcmVhdG9yIjogImRpZDprZXk6ejZNa3M5bTlpZkx3eTNKV3FINGM1N0ViQlFWUzJTcFJDamZhNzl3SGI1dldNNnZoIiwKICAgICJjcmVhdGVkIjogIjIwMjMtMDEtMDZUMDc6NTE6MzZaIiwKICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiZGlkOmtleTp6Nk1rczltOWlmTHd5M0pXcUg0YzU3RWJCUVZTMlNwUkNqZmE3OXdIYjV2V002dmgjejZNa3M5bTlpZkx3eTNKV3FINGM1N0ViQlFWUzJTcFJDamZhNzl3SGI1dldNNnZoIiwKICAgICJqd3MiOiAiZXlKaU5qUWlPbVpoYkhObExDSmpjbWwwSWpwYkltSTJOQ0pkTENKaGJHY2lPaUpGWkVSVFFTSjkuLjZ4U3FvWmphME53akYwYWY5WmtucXgzQ2JoOUdFTnVuQmY5Qzh1TDJ1bEdmd3VzM1VGTV9abmhQald0SFBsLTcyRTlwM0JUNWYycHRab1lrdE1LcERBIgogIH0KfQ" } diff --git a/openapi/api_frontend.go b/openapi/api_frontend.go index eb776ef0..43b27f2b 100644 --- a/openapi/api_frontend.go +++ b/openapi/api_frontend.go @@ -79,7 +79,7 @@ func VerifierPageDisplayQRSIOP(c *gin.Context) { return } - c.HTML(http.StatusOK, "verifier_present_qr", gin.H{"qrcode": qr}) + c.HTML(http.StatusOK, "verifier_present_qr", gin.H{"qrcode": qr, "prefix": getFrontendVerifier().GetPathPrefix()}) } // VerifierLoginQr - Presents a qr as starting point for the auth process @@ -153,6 +153,7 @@ func VerifierLoginQr(c *gin.Context) { "qrExpireAt": qrInfo.ExpireAt.UnixMilli(), "qrDuration": qrInfo.TotalDuration, "authRequest": template.URL(qrInfo.AuthenticationRequest), + "prefix": getFrontendVerifier().GetPathPrefix(), }) } diff --git a/verifier/verifier.go b/verifier/verifier.go index f9a5cc0c..56c79923 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -111,6 +111,7 @@ type Verifier interface { GetOpenIDConfiguration(serviceIdentifier string) (metadata common.OpenIDProviderMetadata, err error) GetRequestObject(state string) (jwt string, err error) GetHost() string + GetPathPrefix() string GetAuthorizationType(clientId string) string GetDefaultScope(serviceIdentifier string) (string, error) // ExchangeRefreshToken atomically consumes a refresh token and returns a @@ -135,6 +136,8 @@ type ValidationService interface { type CredentialVerifier struct { // host of the verifier host string + // path prefix to be prepended to server-built URLs that are not derived from host + pathPrefix string // did of the verifier did string // trusted-issuers-registry to be used for verification @@ -391,34 +394,37 @@ func InitVerifier(config *configModel.Configuration, repo database.ServiceReposi err = nil } + pathPrefix := config.Server.NormalizedPathPrefix() + verifier = &CredentialVerifier{ - (&config.Server).Host, - verifierConfig.Did, - verifierConfig.TirAddress, - key, - sessionCache, - tokenCache, - &randomGenerator{}, - clock, - common.JwtTokenSigner{}, - credentialsConfig, - []ValidationService{ + host: strings.TrimSuffix((&config.Server).Host, "/") + pathPrefix, + pathPrefix: pathPrefix, + did: verifierConfig.Did, + tirAddress: verifierConfig.TirAddress, + signingKey: key, + sessionCache: sessionCache, + tokenCache: tokenCache, + nonceGenerator: &randomGenerator{}, + clock: clock, + tokenSigner: common.JwtTokenSigner{}, + credentialsConfig: credentialsConfig, + validationServices: []ValidationService{ &credentialsVerifier, &externalGaiaXValidator, &trustedParticipantVerificationService, &trustedIssuerVerificationService, &credentialStatusVerificationService, }, - verifierConfig.KeyAlgorithm, - verifierConfig.SupportedModes, - &didSigningKey, - verifierConfig.ClientIdentification, - *verifierConfig, - time.Duration(verifierConfig.JwtExpiration) * time.Minute, - time.Duration(verifierConfig.SessionExpiry), - verifierConfig.RefreshToken.Enabled, - time.Duration(verifierConfig.RefreshToken.Expiration) * time.Minute, - nil, // refreshTokenRepo — set below when enabled + signingAlgorithm: verifierConfig.KeyAlgorithm, + supportedRequestModes: verifierConfig.SupportedModes, + requestSigningKey: &didSigningKey, + clientIdentification: verifierConfig.ClientIdentification, + verifierConfig: *verifierConfig, + jwtExpiration: time.Duration(verifierConfig.JwtExpiration) * time.Minute, + sessionDuration: time.Duration(verifierConfig.SessionExpiry), + refreshTokenEnabled: verifierConfig.RefreshToken.Enabled, + refreshTokenExpiration: time.Duration(verifierConfig.RefreshToken.Expiration) * time.Minute, + refreshTokenRepo: nil, // set below when enabled } logging.Log().Debug("Successfully initalized the verifier") @@ -527,7 +533,7 @@ func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, s return authenticationRequest, err } - authResponseUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) + authResponseUri := fmt.Sprintf("%s://%s%s/api/v1/authentication_response", protocol, host, v.pathPrefix) if requestProtocol == OPENID4VP_PROTOCOL { return v.generateAuthenticationRequest(requestProtocol+"://", clientId, scope, authResponseUri, state, nonce, loginSession, requestMode) } else { @@ -1281,7 +1287,7 @@ func (v *CredentialVerifier) initOid4VPCrossDevice(host string, protocol string, logging.Log().Warnf("Was not able to store the login session %s in cache.", logging.PrettyPrintObject(loginSession)) return authenticationRequest, err } - authResponseUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) + authResponseUri := fmt.Sprintf("%s://%s%s/api/v1/authentication_response", protocol, host, v.pathPrefix) return v.generateAuthenticationRequest("openid4vp://", clientId, scope, authResponseUri, state, nonce, loginSession, requestMode) } @@ -1300,7 +1306,7 @@ func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback logging.Log().Warnf("Was not able to store the login session %s in cache.", logging.PrettyPrintObject(loginSession)) return authenticationRequest, err } - redirectUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) + redirectUri := fmt.Sprintf("%s://%s%s/api/v1/authentication_response", protocol, host, v.pathPrefix) return v.generateAuthenticationRequest("openid4vp://", clientId, "", redirectUri, state, nonce, loginSession, requestMode) } @@ -1630,6 +1636,10 @@ func (v *CredentialVerifier) GetHost() string { return v.host } +func (v *CredentialVerifier) GetPathPrefix() string { + return v.pathPrefix +} + // IsRefreshTokenEnabled reports whether the refresh token feature is active. func (v *CredentialVerifier) IsRefreshTokenEnabled() bool { return v.refreshTokenEnabled diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index bf080f9d..943139c1 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/base64" "encoding/pem" "errors" "math/big" @@ -467,6 +468,70 @@ func TestStartSameDeviceFlow(t *testing.T) { } +// extractResponseUri decodes the (unsigned) JWT payload embedded in a +// REQUEST_MODE_BY_VALUE authentication request and returns its response_uri claim. +func extractResponseUri(t *testing.T, authRequest string) string { + t.Helper() + parsed, err := url.Parse(authRequest) + if err != nil { + t.Fatalf("Was not able to parse authentication request as URL: %v", err) + } + request := parsed.Query().Get("request") + parts := strings.Split(request, ".") + if len(parts) < 2 { + t.Fatalf("Expected a JWT with at least header.payload, got %s", request) + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + t.Fatalf("Was not able to base64-decode the JWT payload: %v", err) + } + var claims map[string]interface{} + if err := json.Unmarshal(payload, &claims); err != nil { + t.Fatalf("Was not able to unmarshal the JWT payload: %v", err) + } + responseUri, _ := claims["response_uri"].(string) + return responseUri +} + +func TestStartSameDeviceFlow_ResponseUriIncludesPathPrefix(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + testKey := getECDSAKey() + sessionCache := mockSessionCache{sessions: map[string]loginSession{}} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomNonce"}} + credentialsConfig := mockCredentialConfig{createMockCredentials("", "", "", "", "", false), nil} + verifier := CredentialVerifier{host: "verifier.org", pathPrefix: "/myservice", did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + + authReq, err := verifier.StartSameDeviceFlow("verifier.org", "https", "my-state", "/redirect", "", "", REQUEST_MODE_BY_VALUE, "", "") + assert.NoError(t, err) + assert.Equal(t, "https://verifier.org/myservice/api/v1/authentication_response", extractResponseUri(t, authReq)) +} + +func TestStartSiopFlow_ResponseUriIncludesPathPrefix(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + testKey := getECDSAKey() + sessionCache := mockSessionCache{sessions: map[string]loginSession{}} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomNonce"}} + credentialsConfig := mockCredentialConfig{createMockCredentials("", "", "", "", "", false), nil} + verifier := CredentialVerifier{host: "verifier.org", pathPrefix: "/myservice", did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + + authReq, err := verifier.StartSiopFlow("verifier.org", "https", "/redirect", "my-state", "", "", REQUEST_MODE_BY_VALUE) + assert.NoError(t, err) + assert.Equal(t, "https://verifier.org/myservice/api/v1/authentication_response", extractResponseUri(t, authReq)) +} + +func TestInitOid4VPCrossDevice_ResponseUriIncludesPathPrefix(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + testKey := getECDSAKey() + sessionCache := mockSessionCache{sessions: map[string]loginSession{}} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomNonce"}} + credentialsConfig := mockCredentialConfig{createMockCredentials("", "", "", "", "", false), nil} + verifier := CredentialVerifier{host: "verifier.org", pathPrefix: "/myservice", did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + + authReq, err := verifier.initOid4VPCrossDevice("verifier.org", "https", "https://wallet.example/callback", "my-state", "", "", "", REQUEST_MODE_BY_VALUE) + assert.NoError(t, err) + assert.Equal(t, "https://verifier.org/myservice/api/v1/authentication_response", extractResponseUri(t, authReq)) +} + type mockExternalSsiKit struct { verificationResults []bool verificationError error @@ -1062,6 +1127,12 @@ func getOpenIdProviderMetadataTests() []openIdProviderMetadataTest { Issuer: verifierHost, ScopesSupported: []string{}, IdTokenSigningAlgValuesSupported: []string{"EdDSA"}}}, + {testName: "Test OIDC metadata with a path prefix baked into host", serviceIdentifier: "serviceId", host: verifierHost + "/myservice", + credentialScopes: map[string]map[string]configModel.ScopeEntry{"serviceId": {}}, mockConfigError: nil, + expectedOpenID: common.OpenIDProviderMetadata{ + Issuer: verifierHost + "/myservice", + ScopesSupported: []string{}, + IdTokenSigningAlgValuesSupported: []string{}}}, } } @@ -1083,10 +1154,19 @@ func TestGetOpenIDConfiguration(t *testing.T) { for _, alg := range tc.expectedOpenID.IdTokenSigningAlgValuesSupported { assert.True(t, slices.Contains(actualOpenID.IdTokenSigningAlgValuesSupported, alg)) } + // the discovery doc derives everything from host, so a path prefix baked + // into host (as InitVerifier does) must propagate to every endpoint URL. + assert.Contains(t, actualOpenID.TokenEndpoint, tc.host) + assert.Contains(t, actualOpenID.JwksUri, tc.host) }) } } +func TestGetPathPrefix(t *testing.T) { + v := CredentialVerifier{pathPrefix: "/myservice"} + assert.Equal(t, "/myservice", v.GetPathPrefix()) +} + func TestRemoveDuplicate(t *testing.T) { type test struct { testName string diff --git a/views/static/js/app.js b/views/static/js/app.js index 450d32c1..f3854bfb 100644 --- a/views/static/js/app.js +++ b/views/static/js/app.js @@ -59,7 +59,8 @@ class LocaleManager { async setLanguage(lang) { try { if (!this.cache.has(lang)) { - const response = await fetch(`/static/locales/${lang}.json`); + const basePath = document.querySelector('meta[name="basePath"]')?.getAttribute('content') || ''; + const response = await fetch(`${basePath}/static/locales/${lang}.json`); const data = await response.json(); this.cache.set(lang, data); } diff --git a/views/verifier_present_qr.html b/views/verifier_present_qr.html index d7379398..78726ddb 100644 --- a/views/verifier_present_qr.html +++ b/views/verifier_present_qr.html @@ -4,19 +4,20 @@ - - - - + + + + + Credential Verifier - + - +
- FIWARE + FIWARE
diff --git a/views/verifier_present_qr_v2.html b/views/verifier_present_qr_v2.html index 2673396e..3c8b99e7 100644 --- a/views/verifier_present_qr_v2.html +++ b/views/verifier_present_qr_v2.html @@ -6,12 +6,13 @@ Verificador OpenID4VC - + - - + + +
- FIWARE + FIWARE