Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"encoding/json"
"strings"

"github.com/fiware/VCVerifier/logging"
)
Expand Down Expand Up @@ -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"`
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
}
5 changes: 0 additions & 5 deletions config/provider.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package config

import (
"fmt"
"os"

"github.com/gookit/config/v2"
"github.com/gookit/config/v2/yaml"
"github.com/mitchellh/mapstructure"
Expand All @@ -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
Expand Down
12 changes: 7 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 != "" {
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
41 changes: 41 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions openapi/api_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions openapi/api_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
3 changes: 2 additions & 1 deletion openapi/api_frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -153,6 +153,7 @@ func VerifierLoginQr(c *gin.Context) {
"qrExpireAt": qrInfo.ExpireAt.UnixMilli(),
"qrDuration": qrInfo.TotalDuration,
"authRequest": template.URL(qrInfo.AuthenticationRequest),
"prefix": getFrontendVerifier().GetPathPrefix(),
})
}

Expand Down
58 changes: 34 additions & 24 deletions verifier/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading