git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: ability to parse JWT encoded profile claims (#3014)

* fix: parse JWT profile claims * Comment with OIDC specs reference * fix: formatting * Updated changelog --------- Co-authored-by: Jan Larwig <jan@larwig.com>

commitb7ff804506c9529d7636e809e1023143d3dd2e14 authorikarius <fred@ikarius.com> dateMon 28 Apr 2025, 16:39 (1 year ago) parent7b41c8e
3 files changed+67−3
CHANGELOG.md+1 pkg/providers/util/claim_extractor.go+18−3 pkg/providers/util/claim_extractor_test.go+48
CHANGELOG.md+1
@@ -12,6 +12,7 @@
1212- [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it)
1313- [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer)
1414- [#1916](https://github.com/oauth2-proxy/oauth2-proxy/pull/1916) fix: role extraction from access token in keycloak oidc (@Elektordi / @tuunit)
15+- [#3014](https://github.com/oauth2-proxy/oauth2-proxy/pull/3014) feat: ability to parse JWT encoded profile claims (@ikarius)
1516
1617# V7.8.2
1718
pkg/providers/util/claim_extractor.go+18−3
@@ -5,6 +5,7 @@ import (
55 "encoding/base64"
66 "encoding/json"
77 "fmt"
8+ "mime"
89 "net/http"
910 "net/url"
1011 "strings"
@@ -94,11 +95,25 @@ func (c *claimExtractor) loadProfileClaims() (*simplejson.Json, error) {
9495 return simplejson.New(), nil
9596 }
9697
97− claims, err := requests.New(c.profileURL.String()).
98+ builder := requests.New(c.profileURL.String()).
9899 WithContext(c.ctx).
99100 WithHeaders(c.requestHeaders).
100− Do().
101− UnmarshalSimpleJSON()
101+ Do()
102+
103+ // We first check if the result is a JWT token
104+ // https://openid.net/specs/openid-connect-core-1_0-final.html#UserInfoResponse
105+ mediaType, _, parseErr := mime.ParseMediaType(builder.Headers().Get("Content-Type"))
106+
107+ if parseErr == nil && mediaType == "application/jwt" {
108+ // Decode and use JWT payload as profile claims
109+ if pl, err := parseJWT(string(builder.Body())); err == nil {
110+ return simplejson.NewJson(pl)
111+ }
112+ }
113+
114+ // Otherwise, process as normal JSON payload
115+ claims, err := builder.UnmarshalSimpleJSON()
116+
102117 if err != nil {
103118 return nil, fmt.Errorf("error making request to profile URL: %v", err)
104119 }
pkg/providers/util/claim_extractor_test.go+48
@@ -497,6 +497,54 @@ var _ = Describe("Claim Extractor Suite", func() {
497497 expectedDst: stringPointer("{\"foo\":[\"bar\",\"baz\"]}"),
498498 }),
499499 )
500+
501+ It("should extract claims from a JWT response", func() {
502+ jwtResponsePayload := `{
503+ "user": "jwtUser",
504+ "email": "jwtEmail",
505+ "groups": [
506+ "jwtGroup1",
507+ "jwtGroup2"
508+ ]
509+ }`
510+
511+ jwtResponseHandler := func(rw http.ResponseWriter, req *http.Request) {
512+ if !hasAuthorizedHeader(req.Header) {
513+ rw.WriteHeader(403)
514+ rw.Write([]byte("Unauthorized"))
515+ return
516+ }
517+
518+ rw.Header().Set("Content-Type", "application/jwt; charset=utf-8")
519+ rw.Write([]byte(createJWTFromPayload(jwtResponsePayload)))
520+ }
521+
522+ claimExtractor, serverClose, err := newTestClaimExtractor(testClaimExtractorOpts{
523+ idTokenPayload: emptyJSON,
524+ setProfileURL: true,
525+ profileRequestHeaders: newAuthorizedHeader(),
526+ profileRequestHandler: jwtResponseHandler,
527+ })
528+ Expect(err).ToNot(HaveOccurred())
529+ if serverClose != nil {
530+ defer serverClose()
531+ }
532+
533+ value, exists, err := claimExtractor.GetClaim("user")
534+ Expect(err).ToNot(HaveOccurred())
535+ Expect(exists).To(BeTrue())
536+ Expect(value).To(Equal("jwtUser"))
537+
538+ value, exists, err = claimExtractor.GetClaim("email")
539+ Expect(err).ToNot(HaveOccurred())
540+ Expect(exists).To(BeTrue())
541+ Expect(value).To(Equal("jwtEmail"))
542+
543+ value, exists, err = claimExtractor.GetClaim("groups")
544+ Expect(err).ToNot(HaveOccurred())
545+ Expect(exists).To(BeTrue())
546+ Expect(value).To(Equal([]interface{}{"jwtGroup1", "jwtGroup2"}))
547+ })
500548})
501549
502550// ******************************************