• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FIWARE / VCVerifier / 25492402266

07 May 2026 11:14AM UTC coverage: 60.916% (+0.5%) from 60.402%
25492402266

push

github

web-flow
Merge pull request #96 from FIWARE/ticket-34/work

291 of 427 new or added lines in 10 files covered. (68.15%)

1 existing line in 1 file now uncovered.

4244 of 6967 relevant lines covered (60.92%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.43
/openapi/api_api.go
1
/*
2
 * vcverifier
3
 *
4
 * Backend component to verify credentials
5
 *
6
 * API version: 0.0.1
7
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
8
 */
9

10
package openapi
11

12
import (
13
        "encoding/base64"
14
        "encoding/json"
15
        "errors"
16
        "fmt"
17
        "net/http"
18
        "slices"
19
        "strings"
20

21
        "github.com/fiware/VCVerifier/common"
22
        "github.com/fiware/VCVerifier/did"
23
        "github.com/fiware/VCVerifier/logging"
24
        "github.com/fiware/VCVerifier/verifier"
25
        "github.com/google/uuid"
26
        "github.com/lestrrat-go/jwx/v3/jwa"
27
        "github.com/lestrrat-go/jwx/v3/jwt"
28

29
        "github.com/gin-gonic/gin"
30
)
31

32
const DEEPLINK = "DEEPLINK"
33
const FRONTEND_V1 = "FRONTEND_V1"
34
const FRONTEND_V2 = "FRONTEND_V2"
35

36
var apiVerifier verifier.Verifier
37
var presentationParser verifier.PresentationParser
38
var sdJwtParser verifier.SdJwtParser
39
var keyResolver verifier.KeyResolver
40

41
var ErrorMessagNoGrantType = ErrorMessage{"no_grant_type_provided", "Token requests require a grant_type."}
42
var ErrorMessageUnsupportedGrantType = ErrorMessage{"unsupported_grant_type", "Provided grant_type is not supported by the implementation."}
43
var ErrorMessageNoCode = ErrorMessage{"no_code_provided", "Token requests require a code."}
44
var ErrorMessageNoRedircetUri = ErrorMessage{"no_redirect_uri_provided", "Token requests require a redirect_uri."}
45
var ErrorMessageNoResource = ErrorMessage{"no_resource_provided", "When using token-exchange, resource is required to provide the client_id."}
46
var ErrorMessageNoState = ErrorMessage{"no_state_provided", "Authentication requires a state provided as query parameter."}
47
var ErrorMessageNoScope = ErrorMessage{"no_scope_provided", "Authentication requires a scope provided as a parameter."}
48
var ErrorMessageInvalidResponseType = ErrorMessage{"invalid_response_type", "Authentication requires the response_type to be `code`."}
49
var ErrorMessageFailedSameDevice = ErrorMessage{"failed_same_device", "Was not able to start a same-device flow."}
50
var ErrorMessageNoClientId = ErrorMessage{"no_client_id_provided", "Authentication requires a client-id provided as a parameter."}
51
var ErrorMessageNoNonce = ErrorMessage{"no_nonce_provided", "Authentication requires a nonce provided as a query parameter."}
52
var ErrorMessageNoToken = ErrorMessage{"no_token_provided", "Authentication requires a token provided as a form parameter."}
53
var ErrorMessageNoPresentationSubmission = ErrorMessage{"no_presentation_submission_provided", "Authentication requires a presentation submission provided as a form parameter."}
54
var ErrorMessageNoCallback = ErrorMessage{"NoCallbackProvided", "A callback address has to be provided as query-parameter."}
55
var ErrorMessageUnableToDecodeToken = ErrorMessage{"invalid_token", "Token could not be decoded."}
56
var ErrorMessageUnableToDecodeCredential = ErrorMessage{"invalid_token", "Could not read the credential(s) inside the token."}
57
var ErrorMessageUnableToDecodeHolder = ErrorMessage{"invalid_token", "Could not read the holder inside the token."}
58
var ErrorMessageNoSuchSession = ErrorMessage{"no_session", "Session with the requested id is not available."}
59
var ErrorMessageInvalidSdJwt = ErrorMessage{"invalid_sdjwt", "SdJwt does not contain all required fields."}
60
var ErrorMessageNoWebsocketConnection = ErrorMessage{"invalid_connection", "No Websocket connection available for the authenticated session."}
61
var ErrorMessageUnresolvableRequestObject = ErrorMessage{"unresolvable_request_object", "Was not able to get the request object from the client."}
62
var ErrorMessageInvalidAudience = ErrorMessage{"invalid_audience", "Audience of the request object was invalid."}
63
var ErrorMessageUnsupportedAssertionType = ErrorMessage{"unsupported_assertion_type", "Assertion type is not supported."}
64
var ErrorMessageInvalidClientAssertion = ErrorMessage{"invalid_client_assertion", "Provided client assertion is invalid."}
65
var ErrorMessageInvalidTokenRequest = ErrorMessage{"invalid_token_request", "Token request has no redirect_uri and no valid client assertion."}
66
var ErrorMessageInvalidSubjectTokenType = ErrorMessage{"invalid_subject_token_type", "Token exchange is only supported for token type urn:eu:oidf:vp_token."}
67
var ErrorMessageInvalidRequestedTokenType = ErrorMessage{"invalid_requested_token_type", "Token exchange is only supported for requesting tokens of type urn:ietf:params:oauth:token-type:access_token."}
68
var ErrorMessageNoRefreshToken = ErrorMessage{"no_refresh_token_provided", "Refresh token requests require a refresh_token."}
69
var ErrorMessageInvalidRefreshToken = ErrorMessage{"invalid_refresh_token", "The provided refresh token is invalid or expired."}
70

71
func getApiVerifier() verifier.Verifier {
1✔
72
        if apiVerifier == nil {
1✔
73
                apiVerifier = verifier.GetVerifier()
×
74
        }
×
75
        return apiVerifier
1✔
76
}
77

78
func getPresentationParser() verifier.PresentationParser {
1✔
79
        if presentationParser == nil {
1✔
80
                presentationParser = verifier.GetPresentationParser()
×
81
        }
×
82
        return presentationParser
1✔
83
}
84

85
func getSdJwtParser() verifier.SdJwtParser {
1✔
86
        if sdJwtParser == nil {
1✔
87
                sdJwtParser = verifier.GetSdJwtParser()
×
88
        }
×
89
        return sdJwtParser
1✔
90
}
91

92
// refreshTokenExpiresIn returns the configured refresh token lifetime in seconds
93
// when a refresh token was issued, or 0 (omitted in JSON) when it was not.
94
func refreshTokenExpiresIn(refreshToken string) int64 {
1✔
95
        if refreshToken == "" {
2✔
96
                return 0
1✔
97
        }
1✔
98
        return getApiVerifier().RefreshTokenExpiresIn()
1✔
99
}
100

101
func getKeyResolver() verifier.KeyResolver {
×
102
        if keyResolver == nil {
×
103
                keyResolver = &verifier.VdrKeyResolver{Vdr: []did.VDR{did.NewKeyVDR(), did.NewJWKVDR(), did.NewWebVDR()}}
×
104
        }
×
105
        return keyResolver
×
106
}
107

108
// GetToken - Token endpoint to exchange the authorization code with the actual JWT.
109
func GetToken(c *gin.Context) {
1✔
110

1✔
111
        logging.Log().Debugf("%v", c.Request)
1✔
112
        grantType, grantTypeExists := c.GetPostForm("grant_type")
1✔
113
        if !grantTypeExists {
2✔
114
                logging.Log().Debug("No grant_type present in the request.")
1✔
115
                c.AbortWithStatusJSON(400, ErrorMessagNoGrantType)
1✔
116
                return
1✔
117
        }
1✔
118

119
        switch grantType {
1✔
120
        case common.TYPE_CODE:
1✔
121
                handleTokenTypeCode(c)
1✔
122
        case common.TYPE_VP_TOKEN:
1✔
123
                handleTokenTypeVPToken(c, c.GetHeader("client_id"))
1✔
124
        case common.TYPE_TOKEN_EXCHANGE:
1✔
125
                resource, resourceExists := c.GetPostForm("resource")
1✔
126
                if !resourceExists {
2✔
127
                        c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoResource)
1✔
128
                        return
1✔
129
                }
1✔
130
                handleTokenTypeTokenExchange(c, resource)
1✔
131
        case common.TYPE_REFRESH_TOKEN:
1✔
132
                handleTokenTypeRefreshToken(c)
1✔
133
        default:
1✔
134
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedGrantType)
1✔
135
        }
136
}
137

138
func AuthorizationEndpoint(c *gin.Context) {
×
139
        logging.Log().Debug("Receive authorization request.")
×
140
        clientId, clientIdExists := c.GetQuery("client_id")
×
141
        responseType, responseTypeExists := c.GetQuery("response_type")
×
142
        scope, scopeExists := c.GetQuery("scope")
×
143
        redirectUri, redirectUriExists := c.GetQuery("redirect_uri")
×
144
        nonce, nonceExists := c.GetQuery("nonce")
×
145
        state, stateExists := c.GetQuery("state")
×
146

×
147
        requestUri, requestUriExists := c.GetQuery("request_uri")
×
148
        if requestUriExists {
×
149
                logging.Log().Debug("Requesting the client for its request object.")
×
150
                cro, err := getRequestObjectClient().GetClientRequestObject(requestUri)
×
151
                if err != nil {
×
152
                        logging.Log().Warnf("Was not able to get request object. Err: %v", err)
×
153
                        c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessageUnresolvableRequestObject)
×
154
                        return
×
155
                }
×
156
                if !slices.Contains(cro.Aud, getFrontendVerifier().GetHost()) {
×
157
                        c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessageInvalidAudience)
×
158
                        return
×
159
                }
×
160

161
                if cro.ClientId != "" {
×
162
                        clientId = cro.ClientId
×
163
                        clientIdExists = true
×
164
                }
×
165

166
                if cro.Scope != "" {
×
167
                        scope = cro.Scope
×
168
                        scopeExists = true
×
169
                }
×
170

171
                if cro.RedirectUri != "" {
×
172
                        redirectUri = cro.RedirectUri
×
173
                        redirectUriExists = true
×
174
                }
×
175

176
                if cro.Nonce != "" {
×
177
                        nonce = cro.Nonce
×
178
                        nonceExists = true
×
179
                }
×
180

181
                if cro.State != "" {
×
182
                        state = cro.State
×
183
                        stateExists = true
×
184
                }
×
185

186
                if cro.ResponseType != "" {
×
187
                        responseType = cro.ResponseType
×
188
                        responseTypeExists = true
×
189
                }
×
190
        }
191

192
        if !clientIdExists {
×
193
                logging.Log().Info("Received an authorization request without a client_id.")
×
194
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoClientId)
×
195
                return
×
196
        }
×
197
        if !scopeExists {
×
198
                logging.Log().Info("Received an authorization request without a scope.")
×
199
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoScope)
×
200
                return
×
201
        }
×
202
        if !redirectUriExists {
×
203
                logging.Log().Info("Received an authorization request without a redirect_uri.")
×
204
                //c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoRedircetUri)
×
205
                //return
×
206
        }
×
207
        if !nonceExists {
×
208
                logging.Log().Info("Received an authorization request without a nonce.")
×
209
                nonce = uuid.NewString()
×
210
        }
×
211
        if !stateExists {
×
212
                logging.Log().Info("Received an authorization request without a state.")
×
213
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState)
×
214
                return
×
215
        }
×
216
        if !responseTypeExists && responseType != "code" {
×
217
                logging.Log().Infof("Received an authorization request with an invalid response type. Was %s.", responseType)
×
218
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidResponseType)
×
219
                return
×
220
        }
×
221

222
        logging.Log().Debugf("Received request: clientID - %s, scope - %s, redirect_uri - %s, nonce - %s, state - %s.", clientId, scope, redirectUri, nonce, state)
×
223

×
224
        protocol := "https"
×
225
        if c.Request.TLS == nil {
×
226
                protocol = "http"
×
227
        }
×
228

229
        authorizationType := getApiVerifier().GetAuthorizationType(clientId)
×
230
        var redirect string
×
231
        var err error
×
232
        switch authorizationType {
×
233
        case DEEPLINK:
×
234
                redirect, err = getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, "", clientId, nonce, verifier.REQUEST_MODE_BY_REFERENCE, scope, verifier.OPENID4VP_PROTOCOL)
×
235
                if err != nil {
×
236
                        logging.Log().Warnf("Was not able start a same device flow. Err: %v", err)
×
237
                        c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessageFailedSameDevice)
×
238
                        return
×
239
                }
×
240
        case FRONTEND_V2:
×
241
                redirect = buildFrontendV2Address(protocol, c.Request.Host, state, clientId, redirectUri, scope, nonce)
×
242
        }
243
        c.Redirect(http.StatusFound, redirect)
×
244
}
245

246
func buildFrontendV2Address(protocol, host, state, clientId, redirectUri, scope, nonce string) string {
×
247
        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)
×
248
        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)
×
249
}
×
250

251
// GetToken - Token endpoint to exchange the authorization code with the actual JWT.
252
func GetTokenForService(c *gin.Context) {
×
253

×
254
        logging.Log().Debugf("%v", c.Request)
×
255
        grantType, grantTypeExists := c.GetPostForm("grant_type")
×
256
        if !grantTypeExists {
×
257
                logging.Log().Debug("No grant_type present in the request.")
×
258
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessagNoGrantType)
×
259
                return
×
260
        }
×
261

262
        switch grantType {
×
263
        case common.TYPE_CODE:
×
264
                handleTokenTypeCode(c)
×
265
        case common.TYPE_VP_TOKEN:
×
266
                handleTokenTypeVPToken(c, c.Param("service_id"))
×
267
        case common.TYPE_TOKEN_EXCHANGE:
×
268
                handleTokenTypeTokenExchange(c, c.Param("service_id"))
×
NEW
269
        case common.TYPE_REFRESH_TOKEN:
×
NEW
270
                handleTokenTypeRefreshToken(c)
×
271
        default:
×
272
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedGrantType)
×
273
        }
274
}
275

276
func handleTokenTypeTokenExchange(c *gin.Context, clientId string) {
1✔
277
        subjectTokenType, subjectTokenTypeExists := c.GetPostForm("subject_token_type")
1✔
278
        if !subjectTokenTypeExists || subjectTokenType != common.TYPE_VP_TOKEN_SUBJECT {
2✔
279
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidSubjectTokenType)
1✔
280
                return
1✔
281
        }
1✔
282
        requestedTokenType, requestedTokenTypeExists := c.GetPostForm("requested_token_type")
1✔
283
        if requestedTokenTypeExists && requestedTokenType != common.TYPE_ACCESS_TOKEN {
2✔
284
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidRequestedTokenType)
1✔
285
                return
1✔
286
        }
1✔
287

288
        scopes := getScopesFromRequest(c, clientId)
1✔
289

1✔
290
        audience, audienceExists := c.GetPostForm("audience")
1✔
291
        if !audienceExists {
2✔
292
                audience = clientId
1✔
293
        }
1✔
294

295
        subjectToken, subjectTokenExists := c.GetPostForm("subject_token")
1✔
296
        if !subjectTokenExists {
2✔
297
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken)
1✔
298
                return
1✔
299
        }
1✔
300

301
        logging.Log().Debugf("Got token %s", subjectToken)
1✔
302

1✔
303
        verifiyVPToken(c, subjectToken, clientId, scopes, audience)
1✔
304
}
305

306
func handleTokenTypeVPToken(c *gin.Context, clientId string) {
1✔
307

1✔
308
        vpToken, vpTokenExists := c.GetPostForm("vp_token")
1✔
309
        if !vpTokenExists {
2✔
310
                logging.Log().Debug("No vp token present in the request.")
1✔
311
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken)
1✔
312
                return
1✔
313
        }
1✔
314

315
        logging.Log().Warnf("Got token %s", vpToken)
1✔
316

1✔
317
        scopes := getScopesFromRequest(c, clientId)
1✔
318
        if len(scopes) == 0 {
1✔
319
                return
×
320
        }
×
321

322
        verifiyVPToken(c, vpToken, clientId, scopes, clientId)
1✔
323
}
324

325
func verifiyVPToken(c *gin.Context, vpToken string, clientId string, scopes []string, audience string) {
1✔
326

1✔
327
        presentation, err := extractVpFromToken(c, vpToken)
1✔
328
        if err != nil {
1✔
329
                logging.Log().Warnf("Was not able to extract the credentials from the vp_token. E: %v", err)
×
330
                return
×
331
        }
×
332

333
        logging.Log().Debug("Was able to extract presentation")
1✔
334
        // Subject is empty since multiple VCs with different subjects can be provided
1✔
335
        expiration, signedToken, err := getApiVerifier().GenerateToken(clientId, "", clientId, scopes, presentation)
1✔
336
        if err != nil {
1✔
337
                logging.Log().Error("Failure during generating M2M token: ", err)
×
338
                c.AbortWithStatusJSON(http.StatusBadRequest, err)
×
339
                return
×
340
        }
×
341

342
        var refreshToken string
1✔
343
        if getApiVerifier().IsRefreshTokenEnabled() {
2✔
344
                refreshToken, err = getApiVerifier().CreateRefreshToken(clientId, signedToken)
1✔
345
                if err != nil {
2✔
346
                        logging.Log().Warnf("Failed to create refresh token: %v", err)
1✔
347
                        // Non-fatal: return the access token without a refresh token.
1✔
348
                }
1✔
349
        }
350

351
        response := TokenResponse{TokenType: "Bearer", IssuedTokenType: common.TYPE_ACCESS_TOKEN, ExpiresIn: float32(expiration), IdToken: signedToken, AccessToken: signedToken, Scope: strings.Join(scopes, ","), RefreshToken: refreshToken, RefreshTokenExpiresIn: refreshTokenExpiresIn(refreshToken)}
1✔
352
        logging.Log().Infof("Generated and signed token: %v", response)
1✔
353
        c.JSON(http.StatusOK, response)
1✔
354
}
355

356
// handleTokenTypeRefreshToken handles the grant_type=refresh_token flow.
357
// It exchanges a valid refresh token for a new access token and a rotated
358
// refresh token.
359
func handleTokenTypeRefreshToken(c *gin.Context) {
1✔
360
        refreshToken, refreshTokenExists := c.GetPostForm("refresh_token")
1✔
361
        if !refreshTokenExists {
2✔
362
                logging.Log().Debug("No refresh_token present in the request.")
1✔
363
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoRefreshToken)
1✔
364
                return
1✔
365
        }
1✔
366

367
        jwtString, expiration, newRefreshToken, err := getApiVerifier().ExchangeRefreshToken(refreshToken)
1✔
368
        if err != nil {
2✔
369
                if errors.Is(err, verifier.ErrorRefreshTokenInvalidSignature) {
1✔
NEW
370
                        logging.Log().Warn("Refresh token exchange rejected: stored JWT signature mismatch")
×
NEW
371
                        c.AbortWithStatusJSON(http.StatusForbidden, ErrorMessageInvalidRefreshToken)
×
372
                } else {
1✔
373
                        logging.Log().Warnf("Failed to exchange refresh token: %v", err)
1✔
374
                        c.AbortWithStatusJSON(http.StatusForbidden, ErrorMessageInvalidRefreshToken)
1✔
375
                }
1✔
376
                return
1✔
377
        }
378

379
        c.JSON(http.StatusOK, TokenResponse{
1✔
380
                TokenType:             "Bearer",
1✔
381
                ExpiresIn:             float32(expiration),
1✔
382
                AccessToken:           jwtString,
1✔
383
                IdToken:               jwtString,
1✔
384
                RefreshToken:          newRefreshToken,
1✔
385
                RefreshTokenExpiresIn: refreshTokenExpiresIn(newRefreshToken),
1✔
386
        })
1✔
387
}
388

389
func handleTokenTypeCode(c *gin.Context) {
1✔
390

1✔
391
        code, codeExists := c.GetPostForm("code")
1✔
392
        if !codeExists {
2✔
393
                logging.Log().Debug("No code present in the request.")
1✔
394
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoCode)
1✔
395
                return
1✔
396
        }
1✔
397

398
        assertionType, assertionTypeExists := c.GetPostForm("client_assertion_type")
1✔
399
        redirectUri, redirectUriExists := c.GetPostForm("redirect_uri")
1✔
400
        if redirectUriExists {
2✔
401
                jwt, expiration, refreshToken, err := getApiVerifier().GetToken(code, redirectUri, false)
1✔
402
                if err != nil {
2✔
403
                        c.AbortWithStatusJSON(http.StatusForbidden, ErrorMessage{Summary: err.Error()})
1✔
404
                        return
1✔
405
                }
1✔
406
                c.JSON(http.StatusOK, TokenResponse{TokenType: "Bearer", ExpiresIn: float32(expiration), IdToken: jwt, AccessToken: jwt, RefreshToken: refreshToken, RefreshTokenExpiresIn: refreshTokenExpiresIn(refreshToken)})
1✔
407
                return
1✔
408
        }
409
        if assertionTypeExists {
1✔
410
                handleWithClientAssertion(c, assertionType, code)
×
411
                return
×
412
        }
×
413
        c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidTokenRequest)
1✔
414
}
415

416
func getScopesFromRequest(c *gin.Context, clientId string) (scopes []string) {
1✔
417

1✔
418
        scope, scopeExists := c.GetPostForm("scope")
1✔
419
        if !scopeExists {
2✔
420
                defaultScope, err := getApiVerifier().GetDefaultScope(clientId)
1✔
421
                if err != nil {
1✔
422
                        c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoScope)
×
423
                        return scopes
×
424
                }
×
425
                logging.Log().Debugf("No scope present in the request, use the default scope %s", defaultScope)
1✔
426
                return []string{defaultScope}
1✔
427
        }
428

429
        return strings.Split(scope, ",")
1✔
430

431
}
432

433
func handleWithClientAssertion(c *gin.Context, assertionType string, code string) {
×
434
        if assertionType != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
×
435
                logging.Log().Warnf("Assertion type %s is not supported.", assertionType)
×
436
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedAssertionType)
×
437
                return
×
438
        }
×
439

440
        clientAssertion, clientAssertionExists := c.GetPostForm("client_assertion")
×
441
        clientId, clientIdExists := c.GetPostForm("client_id")
×
442
        if !clientAssertionExists || !clientIdExists {
×
443
                logging.Log().Warnf("Client Id (%s) or assertion (%s) not provided.", clientId, clientAssertion)
×
444
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedAssertionType)
×
445
                return
×
446
        }
×
447

448
        kid, err := getKeyResolver().ExtractKIDFromJWT(clientAssertion)
×
449
        if err != nil {
×
450
                logging.Log().Warnf("Was not able to retrive kid from token %s. Err: %v.", clientAssertion, err)
×
451
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion)
×
452
                return
×
453
        }
×
454
        pubKey, err := getKeyResolver().ResolvePublicKeyFromDID(kid)
×
455
        if err != nil {
×
456
                logging.Log().Warnf("Was not able to retrive key from kid %s. Err: %v.", kid, err)
×
457
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion)
×
458
                return
×
459
        }
×
460

461
        alg, algExists := pubKey.Algorithm()
×
462
        if !algExists {
×
463
                // fallback to default
×
464
                alg = jwa.ES256()
×
465
        }
×
466

467
        parsed, err := jwt.Parse([]byte(clientAssertion), jwt.WithKey(alg, pubKey))
×
468
        if err != nil {
×
469
                logging.Log().Warnf("Was not able to parse and verify the token %s. Err: %v", clientAssertion, err)
×
470
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion)
×
471
                return
×
472
        }
×
473

474
        // Serialize token to JSON
475
        jsonBytes, err := json.Marshal(parsed)
×
476
        if err != nil {
×
477
                logging.Log().Warnf("Was not able to marshal the token %s. Err: %v", clientAssertion, err)
×
478
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion)
×
479
                return
×
480
        }
×
481

482
        // Unmarshal to your struct
483
        var clientAssertionObject ClientAssertion
×
484
        if err := json.Unmarshal(jsonBytes, &clientAssertionObject); err != nil {
×
485
                logging.Log().Warnf("Was not able to unmarshal the token: %s, Err: %v", string(jsonBytes), err)
×
486
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion)
×
487
                return
×
488
        }
×
489

490
        if clientAssertionObject.Sub != clientId || clientAssertionObject.Iss != clientId || !slices.Contains(clientAssertionObject.Aud, getFrontendVerifier().GetHost()) {
×
491
                logging.Log().Warnf("Invalid assertion: %s. Client Id: %s, Host: %s", logging.PrettyPrintObject(clientAssertionObject), clientId, getApiVerifier().GetHost())
×
492
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion)
×
493
                return
×
494
        }
×
495

NEW
496
        jwt, expiration, refreshToken, err := getApiVerifier().GetToken(code, "", true)
×
497
        if err != nil {
×
498
                c.AbortWithStatusJSON(http.StatusForbidden, ErrorMessage{Summary: err.Error()})
×
499
                return
×
500
        }
×
NEW
501
        c.JSON(http.StatusOK, TokenResponse{TokenType: "Bearer", ExpiresIn: float32(expiration), IdToken: jwt, AccessToken: jwt, RefreshToken: refreshToken, RefreshTokenExpiresIn: refreshTokenExpiresIn(refreshToken)})
×
502
}
503

504
// StartSIOPSameDevice - Starts the siop flow for credentials hold by the same device
505
func StartSIOPSameDevice(c *gin.Context) {
1✔
506
        state, stateExists := c.GetQuery("state")
1✔
507
        if !stateExists {
2✔
508
                logging.Log().Debugf("No state was provided.")
1✔
509
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessage{"no_state_provided", "Authentication requires a state provided as query parameter."})
1✔
510
                return
1✔
511
        }
1✔
512
        redirectPath, redirectPathExists := c.GetQuery("redirect_path")
1✔
513
        requestProtocol := verifier.REDIRECT_PROTOCOL
1✔
514
        if !redirectPathExists {
2✔
515
                requestProtocol = verifier.OPENID4VP_PROTOCOL
1✔
516
        }
1✔
517

518
        protocol := "https"
1✔
519
        if c.Request.TLS == nil {
2✔
520
                protocol = "http"
1✔
521
        }
1✔
522

523
        clientId, clientIdExists := c.GetQuery("client_id")
1✔
524
        logging.Log().Debugf("The client id %s", clientId)
1✔
525
        if !clientIdExists {
2✔
526
                logging.Log().Infof("Start a login flow for a not specified client.")
1✔
527
        }
1✔
528

529
        requestMode, requestModeExists := c.GetQuery("request_mode")
1✔
530
        if !requestModeExists {
2✔
531
                logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE)
1✔
532
                requestMode = DEFAULT_REQUEST_MODE
1✔
533
        }
1✔
534

535
        scope, scopeExists := c.GetQuery("scope")
1✔
536
        if !scopeExists {
2✔
537
                logging.Log().Infof("Start a login flow with default scope.")
1✔
538
                scope = ""
1✔
539
        }
1✔
540

541
        authenticationRequest, err := getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, redirectPath, clientId, "", requestMode, scope, requestProtocol)
1✔
542
        if err != nil {
2✔
543
                logging.Log().Warnf("Error starting the same-device flow. Err: %v", err)
1✔
544
                c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessage{err.Error(), "Was not able to start the same device flow."})
1✔
545
                return
1✔
546
        }
1✔
547

548
        c.Redirect(http.StatusFound, authenticationRequest)
1✔
549
}
550

551
// VerifierAPIAuthenticationResponse - Stores the credential for the given session
552
func VerifierAPIAuthenticationResponse(c *gin.Context) {
1✔
553

1✔
554
        var state string
1✔
555
        stateForm, stateFormExists := c.GetPostForm("state")
1✔
556
        stateQuery, stateQueryExists := c.GetQuery("state")
1✔
557
        if !stateFormExists && !stateQueryExists {
2✔
558
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState)
1✔
559
                return
1✔
560
        }
1✔
561
        if stateFormExists {
2✔
562
                state = stateForm
1✔
563
        } else {
1✔
564
                // allow the state submitted through a query parameter for backwards-compatibility
×
565
                state = stateQuery
×
566
        }
×
567

568
        vptoken, tokenExists := c.GetPostForm("vp_token")
1✔
569
        if !tokenExists {
2✔
570
                logging.Log().Info("No token was provided.")
1✔
571
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken)
1✔
572
                return
1✔
573
        }
1✔
574

575
        presentation, err := extractVpFromToken(c, vptoken)
1✔
576
        if err != nil {
1✔
577
                logging.Log().Warnf("Was not able to extract the presentation from the vp_token.")
×
578
                return
×
579
        }
×
580
        handleAuthenticationResponse(c, state, presentation)
1✔
581
}
582

583
// GetVerifierAPIAuthenticationResponse - Stores the credential for the given session
584
func GetVerifierAPIAuthenticationResponse(c *gin.Context) {
×
585
        state, stateExists := c.GetQuery("state")
×
586
        if !stateExists {
×
587
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState)
×
588
                return
×
589
        }
×
590

591
        vpToken, tokenExists := c.GetQuery("vp_token")
×
592
        if !tokenExists {
×
593
                logging.Log().Info("No token was provided.")
×
594
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken)
×
595
                return
×
596
        }
×
597
        presentation, err := extractVpFromToken(c, vpToken)
×
598
        if err != nil {
×
599
                logging.Log().Warnf("Was not able to extract the presentation from the vp_token.")
×
600
                return
×
601
        }
×
602
        handleAuthenticationResponse(c, state, presentation)
×
603
}
604

605
// GetRequestByReference - Get the request object by reference
606
func GetRequestByReference(c *gin.Context) {
×
607
        sessionId := c.Param("id")
×
608

×
609
        jwt, err := verifier.GetVerifier().GetRequestObject(sessionId)
×
610
        if err != nil {
×
611
                logging.Log().Debugf("No request for  %s. Err: %v", sessionId, err)
×
612
                c.AbortWithStatusJSON(http.StatusNotFound, ErrorMessageNoSuchSession)
×
613
                return
×
614
        }
×
615
        c.String(http.StatusOK, jwt)
×
616
}
617

618
func extractVpFromToken(c *gin.Context, vpToken string) (parsedPresentation *common.Presentation, err error) {
1✔
619

1✔
620
        logging.Log().Debugf("The token %s.", vpToken)
1✔
621

1✔
622
        parsedPresentation, err = getPresentationFromQuery(c, vpToken)
1✔
623

1✔
624
        if err != nil {
1✔
625
                logging.Log().Debugf("Received a vpToken with a query, but was not able to extract the presentation. Token: %s", vpToken)
×
626
                return parsedPresentation, err
×
627
        }
×
628
        if parsedPresentation != nil {
1✔
629
                return parsedPresentation, err
×
630
        }
×
631
        return tokenToPresentation(c, vpToken)
1✔
632

633
}
634

635
func tokenToPresentation(c *gin.Context, vpToken string) (parsedPresentation *common.Presentation, err error) {
1✔
636
        tokenBytes := decodeVpString(vpToken)
1✔
637

1✔
638
        isSdJWT, parsedPresentation, err := isSdJWT(c, vpToken)
1✔
639
        if isSdJWT && err != nil {
1✔
640
                return
×
641
        }
×
642
        if isSdJWT {
2✔
643
                logging.Log().Debugf("Received an sdJwt: %s", logging.PrettyPrintObject(parsedPresentation))
1✔
644
                return
1✔
645
        }
1✔
646

647
        parsedPresentation, err = getSdJwtParser().ParseWithSdJwt(tokenBytes)
1✔
648
        if err == nil {
1✔
649
                logging.Log().Debug("Parsed presentation containing sd-jwt's.")
×
650
                return parsedPresentation, err
×
651
        }
×
652
        if err == verifier.ErrorInvalidProof {
1✔
653
                logging.Log().Infof("Was not able to parse the token %s. Err: %v", vpToken, err)
×
654
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnableToDecodeToken)
×
655
                return
×
656
        }
×
657
        logging.Log().Debugf("Parse without SD-Jwt %v", err)
1✔
658

1✔
659
        logging.Log().Debug("Parse presentation.")
1✔
660

1✔
661
        parsedPresentation, err = getPresentationParser().ParsePresentation(tokenBytes)
1✔
662

1✔
663
        if err != nil {
1✔
664
                logging.Log().Infof("Was not able to parse the token %s. Err: %v", vpToken, err)
×
665
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnableToDecodeToken)
×
666
                return
×
667
        }
×
668

669
        return
1✔
670
}
671

672
func getPresentationFromQuery(c *gin.Context, vpToken string) (parsedPresentation *common.Presentation, err error) {
1✔
673
        tokenBytes := decodeVpString(vpToken)
1✔
674

1✔
675
        var queryMap map[string]string
1✔
676
        //unmarshal
1✔
677
        err = json.Unmarshal(tokenBytes, &queryMap)
1✔
678
        if err != nil {
2✔
679
                logging.Log().Debug("VP Token does not contain query map. Checking the other options.", err)
1✔
680
                return nil, nil
1✔
681
        }
1✔
682

683
        for _, v := range queryMap {
×
684
                p, err := tokenToPresentation(c, v)
×
685
                if err != nil {
×
686
                        return nil, err
×
687
                }
×
688
                if parsedPresentation == nil {
×
689
                        parsedPresentation = p
×
690
                } else {
×
691
                        parsedPresentation.AddCredentials(p.Credentials()...)
×
692
                }
×
693
        }
694
        return parsedPresentation, err
×
695
}
696

697
// checks if the presented token contains a single sd-jwt credential. Will be repackage to a presentation for further validation
698
func isSdJWT(c *gin.Context, vpToken string) (isSdJwt bool, presentation *common.Presentation, err error) {
1✔
699
        claims, err := getSdJwtParser().Parse(vpToken)
1✔
700
        if err != nil {
2✔
701
                logging.Log().Debugf("Was not a sdjwt. Err: %v", err)
1✔
702
                return false, presentation, err
1✔
703
        }
1✔
704
        issuer, i_ok := claims[common.JWTClaimIss]
1✔
705
        vct, vct_ok := claims[common.JWTClaimVct]
1✔
706
        if !i_ok || !vct_ok {
2✔
707
                // Not an SD-JWT VC (missing iss or vct) — let other parsers handle it
1✔
708
                logging.Log().Debugf("Token does not contain issuer(%v) or vct(%v), not an SD-JWT VC.", i_ok, vct_ok)
1✔
709
                return false, presentation, nil
1✔
710
        }
1✔
711
        customFields := common.CustomFields{}
1✔
712
        for k, v := range claims {
2✔
713
                if k != common.JWTClaimIss && k != common.JWTClaimVct {
2✔
714
                        customFields[k] = v
1✔
715
                }
1✔
716
        }
717
        subject := common.Subject{CustomFields: customFields}
1✔
718
        contents := common.CredentialContents{Issuer: &common.Issuer{ID: issuer.(string)}, Types: []string{vct.(string)}, Subject: []common.Subject{subject}}
1✔
719
        credential, err := common.CreateCredential(contents, common.CustomFields{})
1✔
720
        if err != nil {
1✔
721
                logging.Log().Infof("Was not able to create credential from sdJwt. E: %v", err)
×
722
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidSdJwt)
×
723
                return true, presentation, err
×
724
        }
×
725
        presentation, err = common.NewPresentation()
1✔
726
        if err != nil {
1✔
727
                logging.Log().Infof("Was not able to create credpresentation from sdJwt. E: %v", err)
×
728
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidSdJwt)
×
729
                return true, presentation, err
×
730
        }
×
731
        presentation.AddCredentials(credential)
1✔
732
        presentation.Holder = issuer.(string)
1✔
733
        return true, presentation, nil
1✔
734
}
735

736
// decodeVpString - In newer versions of OID4VP the token is not encoded as a whole but only its segments separately. This function covers the older and newer versions
737
func decodeVpString(vpToken string) (tokenBytes []byte) {
1✔
738
        tokenBytes, err := base64.RawURLEncoding.DecodeString(vpToken)
1✔
739
        if err != nil {
2✔
740
                return []byte(vpToken)
1✔
741
        }
1✔
742
        return tokenBytes
1✔
743
}
744

745
func handleAuthenticationResponse(c *gin.Context, state string, presentation *common.Presentation) {
1✔
746

1✔
747
        response, err := getApiVerifier().AuthenticationResponse(state, presentation)
1✔
748
        if err != nil {
2✔
749
                logging.Log().Warnf("Was not able to fullfil the authentication response. Err: %v", err)
1✔
750
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessage{Summary: err.Error()})
1✔
751
                return
1✔
752
        }
1✔
753
        if response != (verifier.Response{}) && response.FlowVersion == verifier.SAME_DEVICE {
2✔
754
                if response.Nonce != "" {
1✔
755
                        c.Redirect(302, fmt.Sprintf("%s?state=%s&code=%s&nonce=%s", response.RedirectTarget, response.SessionId, response.Code, response.Nonce))
×
756
                } else {
1✔
757
                        c.Redirect(302, fmt.Sprintf("%s?state=%s&code=%s", response.RedirectTarget, response.SessionId, response.Code))
1✔
758
                }
1✔
759
                return
1✔
760
        } else if response != (verifier.Response{}) && response.FlowVersion == verifier.CROSS_DEVICE_V2 {
×
761
                sendRedirect(c, response.SessionId, response.Code, response.RedirectTarget)
×
762
        }
×
763
        logging.Log().Debugf("Successfully authenticated %s.", state)
×
764
        c.JSON(http.StatusOK, gin.H{})
×
765
}
766

767
// VerifierAPIJWKS - Provides the public keys for the given verifier, to be used for verifing the JWTs
768
func VerifierAPIJWKS(c *gin.Context) {
×
769
        c.JSON(http.StatusOK, getApiVerifier().GetJWKS())
×
770
}
×
771

772
// VerifierAPIOpenID
773
func VerifierAPIOpenIDConfiguration(c *gin.Context) {
×
774

×
775
        metadata, err := getApiVerifier().GetOpenIDConfiguration(c.Param("service_id"))
×
776
        if err != nil {
×
777
                c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessage{err.Error(), "Was not able to generate the OpenID metadata."})
×
778
                return
×
779
        }
×
780
        c.JSON(http.StatusOK, metadata)
×
781
}
782

783
// VerifierAPIStartSIOP - Initiates the siop flow and returns the 'openid://...' connection string
784
func VerifierAPIStartSIOP(c *gin.Context) {
1✔
785
        state, stateExists := c.GetQuery("state")
1✔
786
        if !stateExists {
2✔
787
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState)
1✔
788
                // early exit
1✔
789
                return
1✔
790
        }
1✔
791

792
        callback, callbackExists := c.GetQuery("client_callback")
1✔
793
        if !callbackExists {
2✔
794
                c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoCallback)
1✔
795
                // early exit
1✔
796
                return
1✔
797
        }
1✔
798
        protocol := "https"
1✔
799
        if c.Request.TLS == nil {
2✔
800
                protocol = "http"
1✔
801
        }
1✔
802
        clientId, clientIdExists := c.GetQuery("client_id")
1✔
803
        if !clientIdExists {
2✔
804
                logging.Log().Infof("Start a login flow for a not specified client.")
1✔
805
        }
1✔
806

807
        requestMode, requestModeExists := c.GetQuery("request_mode")
1✔
808
        if !requestModeExists {
2✔
809
                logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE)
1✔
810
                requestMode = DEFAULT_REQUEST_MODE
1✔
811
        }
1✔
812

813
        connectionString, err := getApiVerifier().StartSiopFlow(c.Request.Host, protocol, callback, state, clientId, "", requestMode)
1✔
814
        if err != nil {
2✔
815
                c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessage{err.Error(), "Was not able to generate the connection string."})
1✔
816
                return
1✔
817
        }
1✔
818
        c.String(http.StatusOK, connectionString)
1✔
819
}
820

821
type ClientAssertion struct {
822
        Iss string   `json:"iss"`
823
        Aud []string `json:"aud"`
824
        Sub string   `json:"sub"`
825
        Exp int      `json:"exp"`
826
        Iat int      `json:"iat"`
827
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc