Skip to content

Commit 646e24d

Browse files
feat(oidc): support access token in body for user info post (#769)
1 parent 0d286d1 commit 646e24d

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 no 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+
router.ServeHTTP(recorder, req)
444+
assert.Equal(t, 401, recorder.Code)
445+
446+
var res map[string]any
447+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
448+
assert.NoError(t, err)
449+
assert.Equal(t, "invalid_request", res["error"])
450+
},
451+
},
452+
{
453+
description: "Ensure userinfo forbids access with malformed authorization header",
454+
middlewares: []gin.HandlerFunc{},
455+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
456+
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
457+
req.Header.Set("Authorization", "Bearer")
458+
router.ServeHTTP(recorder, req)
459+
assert.Equal(t, 401, recorder.Code)
460+
461+
var res map[string]any
462+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
463+
assert.NoError(t, err)
464+
assert.Equal(t, "invalid_request", res["error"])
465+
},
466+
},
467+
{
468+
description: "Ensure userinfo forbids access with invalid token type",
469+
middlewares: []gin.HandlerFunc{},
470+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
471+
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
472+
req.Header.Set("Authorization", "Basic some-token")
473+
router.ServeHTTP(recorder, req)
474+
assert.Equal(t, 401, recorder.Code)
475+
476+
var res map[string]any
477+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
478+
assert.NoError(t, err)
479+
assert.Equal(t, "invalid_request", res["error"])
480+
},
481+
},
482+
{
483+
description: "Ensure userinfo forbids access with empty bearer token",
484+
middlewares: []gin.HandlerFunc{},
485+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
486+
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
487+
req.Header.Set("Authorization", "Bearer ")
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_grant", 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 POST rejects wrong content type",
514+
middlewares: []gin.HandlerFunc{},
515+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
516+
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(`{"access_token":"some-token"}`))
517+
req.Header.Set("Content-Type", "application/json")
518+
router.ServeHTTP(recorder, req)
519+
assert.Equal(t, 400, recorder.Code)
520+
521+
var res map[string]any
522+
err := json.Unmarshal(recorder.Body.Bytes(), &res)
523+
assert.NoError(t, err)
524+
assert.Equal(t, "invalid_request", res["error"])
525+
},
526+
},
527+
{
528+
description: "Ensure userinfo accepts access token via POST body",
529+
middlewares: []gin.HandlerFunc{
530+
simpleCtx,
531+
},
532+
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
533+
tokenTest, found := getTestByDescription("Ensure we can get a token with a valid request")
534+
assert.True(t, found, "Token test not found")
535+
tokenRecorder := httptest.NewRecorder()
536+
tokenTest(t, router, tokenRecorder)
537+
538+
var tokenRes map[string]any
539+
err := json.Unmarshal(tokenRecorder.Body.Bytes(), &tokenRes)
540+
assert.NoError(t, err)
541+
542+
accessToken := tokenRes["access_token"].(string)
543+
assert.NotEmpty(t, accessToken)
544+
545+
body := url.Values{}
546+
body.Set("access_token", accessToken)
547+
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(body.Encode()))
548+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
549+
router.ServeHTTP(recorder, req)
550+
assert.Equal(t, 200, recorder.Code)
551+
552+
var userInfoRes map[string]any
553+
err = json.Unmarshal(recorder.Body.Bytes(), &userInfoRes)
554+
assert.NoError(t, err)
555+
556+
_, ok := userInfoRes["sub"]
557+
assert.True(t, ok, "Expected sub claim in userinfo response")
558+
},
559+
},
438560
{
439561
description: "Ensure plain PKCE succeeds",
440562
middlewares: []gin.HandlerFunc{

0 commit comments

Comments
 (0)