Skip to content

Commit dd448c9

Browse files
committed
feat(oidc): support access token in body for user info post
1 parent 0d286d1 commit dd448c9

2 files changed

Lines changed: 160 additions & 11 deletions

File tree

internal/controller/oidc_controller.go

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/gin-gonic/gin"
1111
"github.com/google/go-querystring/query"
12+
1213
"github.com/steveiliop56/tinyauth/internal/service"
1314
"github.com/steveiliop56/tinyauth/internal/utils"
1415
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
@@ -376,22 +377,48 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
376377
return
377378
}
378379

380+
var token string
381+
379382
authorization := c.GetHeader("Authorization")
383+
if authorization != "" {
384+
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
385+
if !ok {
386+
tlog.App.Warn().Msg("OIDC userinfo accessed with malformed authorization header")
387+
c.JSON(401, gin.H{
388+
"error": "invalid_request",
389+
})
390+
return
391+
}
380392

381-
tokenType, token, ok := strings.Cut(authorization, " ")
393+
if strings.ToLower(tokenType) != "bearer" {
394+
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
395+
c.JSON(401, gin.H{
396+
"error": "invalid_request",
397+
})
398+
return
399+
}
382400

383-
if !ok {
401+
token = bearerToken
402+
} else if c.Request.Method == http.MethodPost {
403+
if c.ContentType() != "application/x-www-form-urlencoded" {
404+
tlog.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
405+
c.JSON(400, gin.H{
406+
"error": "invalid_request",
407+
})
408+
return
409+
}
410+
token = c.PostForm("access_token")
411+
if token == "" {
412+
tlog.App.Warn().Msg("OIDC userinfo POST accessed without access_token in body")
413+
c.JSON(401, gin.H{
414+
"error": "invalid_request",
415+
})
416+
return
417+
}
418+
} else {
384419
tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header")
385420
c.JSON(401, gin.H{
386-
"error": "invalid_grant",
387-
})
388-
return
389-
}
390-
391-
if strings.ToLower(tokenType) != "bearer" {
392-
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
393-
c.JSON(401, gin.H{
394-
"error": "invalid_grant",
421+
"error": "invalid_request",
395422
})
396423
return
397424
}

internal/controller/oidc_controller_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,128 @@ func TestOIDCController(t *testing.T) {
435435
assert.False(t, ok, "Did not expect email claim in userinfo response")
436436
},
437437
},
438+
{
439+
description: "Ensure userinfo forbids access with malformed authorization header",
440+
middlewares: []gin.HandlerFunc{},
441+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
442+
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
443+
req.Header.Set("Authorization", "Bearer")
444+
router.ServeHTTP(recorder, req)
445+
assert.Equal(t, 401, recorder.Code)
446+
447+
var res map[string]any
448+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
449+
assert.NoError(t, err)
450+
assert.Equal(t, "invalid_request", res["error"])
451+
},
452+
},
453+
{
454+
description: "Ensure userinfo forbids access with invalid token type",
455+
middlewares: []gin.HandlerFunc{},
456+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
457+
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
458+
req.Header.Set("Authorization", "Basic some-token")
459+
router.ServeHTTP(recorder, req)
460+
assert.Equal(t, 401, recorder.Code)
461+
462+
var res map[string]any
463+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
464+
assert.NoError(t, err)
465+
assert.Equal(t, "invalid_request", res["error"])
466+
},
467+
},
468+
{
469+
description: "Ensure userinfo forbids access with empty bearer token",
470+
middlewares: []gin.HandlerFunc{},
471+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
472+
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
473+
req.Header.Set("Authorization", "Bearer ")
474+
router.ServeHTTP(recorder, req)
475+
assert.Equal(t, 401, recorder.Code)
476+
477+
var res map[string]any
478+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
479+
assert.NoError(t, err)
480+
assert.Equal(t, "invalid_grant", res["error"])
481+
},
482+
},
483+
{
484+
description: "Ensure userinfo forbids access with no authorization header",
485+
middlewares: []gin.HandlerFunc{},
486+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
487+
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
488+
router.ServeHTTP(recorder, req)
489+
assert.Equal(t, 401, recorder.Code)
490+
491+
var res map[string]any
492+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
493+
assert.NoError(t, err)
494+
assert.Equal(t, "invalid_request", res["error"])
495+
},
496+
},
497+
{
498+
description: "Ensure userinfo POST rejects missing access token in body",
499+
middlewares: []gin.HandlerFunc{},
500+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
501+
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(""))
502+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
503+
router.ServeHTTP(recorder, req)
504+
assert.Equal(t, 401, recorder.Code)
505+
506+
var res map[string]any
507+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
508+
assert.NoError(t, err)
509+
assert.Equal(t, "invalid_request", res["error"])
510+
},
511+
},
512+
{
513+
description: "Ensure userinfo accepts access token via POST body",
514+
middlewares: []gin.HandlerFunc{
515+
simpleCtx,
516+
},
517+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
518+
tokenTest, found := getTestByDescription("Ensure we can get a token with a valid request")
519+
assert.True(t, found, "Token test not found")
520+
tokenRecorder := httptest.NewRecorder()
521+
tokenTest(t, router, tokenRecorder)
522+
523+
var tokenRes map[string]any
524+
err := json.Unmarshal(tokenRecorder.Body.Bytes(), &tokenRes)
525+
assert.NoError(t, err)
526+
527+
accessToken := tokenRes["access_token"].(string)
528+
assert.NotEmpty(t, accessToken)
529+
530+
body := url.Values{}
531+
body.Set("access_token", accessToken)
532+
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(body.Encode()))
533+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
534+
router.ServeHTTP(recorder, req)
535+
assert.Equal(t, 200, recorder.Code)
536+
537+
var userInfoRes map[string]any
538+
err = json.Unmarshal(recorder.Body.Bytes(), &userInfoRes)
539+
assert.NoError(t, err)
540+
541+
_, ok := userInfoRes["sub"]
542+
assert.True(t, ok, "Expected sub claim in userinfo response")
543+
},
544+
},
545+
{
546+
description: "Ensure userinfo POST rejects wrong content type",
547+
middlewares: []gin.HandlerFunc{},
548+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
549+
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(`{"access_token":"some-token"}`))
550+
req.Header.Set("Content-Type", "application/json")
551+
router.ServeHTTP(recorder, req)
552+
assert.Equal(t, 400, recorder.Code)
553+
554+
var res map[string]any
555+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
556+
assert.NoError(t, err)
557+
assert.Equal(t, "invalid_request", res["error"])
558+
},
559+
},
438560
{
439561
description: "Ensure plain PKCE succeeds",
440562
middlewares: []gin.HandlerFunc{

0 commit comments

Comments
 (0)