From 2eecf756e43a2d77e489d41251af4ab2f419aa06 Mon Sep 17 00:00:00 2001 From: Ryan Luckie Date: Fri, 3 May 2019 17:33:56 -0500 Subject: [PATCH 1/7] Add OIDC support for UserInfo Endpoint Email Verification * Current OIDC implementation asserts that user email check must come from JWT token claims. OIDC specification also allows for source of user email to be fetched from userinfo profile endpoint. http://openid.net/specs/openid-connect-core-1_0.html#UserInfo * First, attempt to retrieve email from JWT token claims. Then fall back to requesting email from userinfo endpoint. * Don't fallback to subject for email https://github.com/bitly/oauth2_proxy/pull/481 --- providers/oidc.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/providers/oidc.go b/providers/oidc.go index 86a58f6..396310f 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -3,11 +3,15 @@ package providers import ( "context" "fmt" + "net/http" "time" oidc "github.com/coreos/go-oidc" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/requests" + "golang.org/x/oauth2" + ) // OIDCProvider represents an OIDC based Identity Provider @@ -117,8 +121,31 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok } if claims.Email == "" { - // TODO: Try getting email from /userinfo before falling back to Subject - claims.Email = claims.Subject + if p.ProfileURL.String() == "" { + return nil, fmt.Errorf("id_token did not contain an email") + } + + // If the userinfo endpoint profileURL is defined, then there is a chance the userinfo + // contents at the profileURL contains the email. + // Make a query to the userinfo endpoint, and attempt to locate the email from there. + + req, err := http.NewRequest("GET", p.ProfileURL.String(), nil) + if err != nil { + return nil, err + } + req.Header = getOIDCHeader(token.AccessToken) + + json, err := requests.Request(req) + if err != nil { + return nil, err + } + + email, err := json.Get("email").String() + if err != nil { + return nil, fmt.Errorf("id_token nor userinfo endpoint did not contain an email") + } + + claims.Email = email } if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified { return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email) @@ -145,3 +172,10 @@ func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool { return true } + +func getOIDCHeader(access_token string) http.Header { + header := make(http.Header) + header.Set("Accept", "application/json") + header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token)) + return header +} From 0d94f5e51597628a5dc920a723290a73e6d59695 Mon Sep 17 00:00:00 2001 From: Ryan Luckie Date: Fri, 3 May 2019 23:07:48 +0000 Subject: [PATCH 2/7] fix lint error --- providers/oidc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/oidc.go b/providers/oidc.go index 396310f..937a16b 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -173,9 +173,9 @@ func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool { return true } -func getOIDCHeader(access_token string) http.Header { +func getOIDCHeader(accessToken string) http.Header { header := make(http.Header) header.Set("Accept", "application/json") - header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token)) + header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) return header } From 122ec45dd8f3da63d74a91fcd31e6ed5e11bdf49 Mon Sep 17 00:00:00 2001 From: Ryan Luckie Date: Tue, 7 May 2019 16:17:38 -0500 Subject: [PATCH 3/7] Requested changes --- providers/oidc.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/providers/oidc.go b/providers/oidc.go index 937a16b..7b3df62 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -11,7 +11,6 @@ import ( "github.com/pusher/oauth2_proxy/pkg/requests" "golang.org/x/oauth2" - ) // OIDCProvider represents an OIDC based Identity Provider @@ -135,12 +134,12 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok } req.Header = getOIDCHeader(token.AccessToken) - json, err := requests.Request(req) + respJson, err := requests.Request(req) if err != nil { return nil, err } - email, err := json.Get("email").String() + email, err := respJson.Get("email").String() if err != nil { return nil, fmt.Errorf("id_token nor userinfo endpoint did not contain an email") } From f537720b5290e8e9b8d222fe56c77567b98f4746 Mon Sep 17 00:00:00 2001 From: Ryan Luckie Date: Tue, 7 May 2019 16:29:21 -0500 Subject: [PATCH 4/7] fix lint errors --- providers/oidc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/oidc.go b/providers/oidc.go index 7b3df62..3eaac15 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -134,12 +134,12 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok } req.Header = getOIDCHeader(token.AccessToken) - respJson, err := requests.Request(req) + respJSON, err := requests.Request(req) if err != nil { return nil, err } - email, err := respJson.Get("email").String() + email, err := respJSON.Get("email").String() if err != nil { return nil, fmt.Errorf("id_token nor userinfo endpoint did not contain an email") } From 93cb575d7c94ea8ec5056c100fd0a4779ffcd8c5 Mon Sep 17 00:00:00 2001 From: Ryan Luckie Date: Fri, 19 Jul 2019 08:59:29 -0500 Subject: [PATCH 5/7] Fix error message for clarity --- providers/oidc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/oidc.go b/providers/oidc.go index 3eaac15..2abf2ca 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -141,7 +141,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok email, err := respJSON.Get("email").String() if err != nil { - return nil, fmt.Errorf("id_token nor userinfo endpoint did not contain an email") + return nil, fmt.Errorf("Neither id_token nor userinfo endpoint contained an email") } claims.Email = email From 4a6b703c543b72e60421ca597749cd08780701e8 Mon Sep 17 00:00:00 2001 From: Ryan Luckie Date: Fri, 19 Jul 2019 09:03:01 -0500 Subject: [PATCH 6/7] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f09c2..4166882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ - [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email` - [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore) - [#211](https://github.com/pusher/oauth2_proxy/pull/211) Switch from dep to go modules (@steakunderscore) +- [#145](https://github.com/pusher/oauth2_proxy/pull/145) Add support for OIDC UserInfo endpoint email verification (@rtluckie) # v3.2.0 From fb52bdb90cbf638b65c900c05582eace43b23223 Mon Sep 17 00:00:00 2001 From: ferhat elmas Date: Tue, 13 Aug 2019 12:42:23 +0200 Subject: [PATCH 7/7] Fix some typos --- docs/configuration/configuration.md | 4 ++-- main.go | 4 ++-- oauthproxy.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index ea677d5..5e06883 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -82,8 +82,8 @@ Usage of oauth2_proxy: -redeem-url string: Token redemption endpoint -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" -redis-connection-url string: URL of redis server for redis session storage (eg: redis://HOST[:PORT]) - -redis-sentinel-master-name string: Redis sentinel master name. Used in conjuction with --redis-use-sentinel - -redis-sentinel-connection-urls: List of Redis sentinel conneciton URLs (eg redis://HOST[:PORT]). Used in conjuction with --redis-use-sentinel + -redis-sentinel-master-name string: Redis sentinel master name. Used in conjunction with --redis-use-sentinel + -redis-sentinel-connection-urls: List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel -redis-use-sentinel: Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature (default: false) -request-logging: Log requests to stdout (default true) -request-logging-format: Template for request log lines (see "Logging Configuration" paragraph below) diff --git a/main.go b/main.go index 823a16d..872990a 100644 --- a/main.go +++ b/main.go @@ -86,8 +86,8 @@ func main() { flagSet.String("session-store-type", "cookie", "the session storage provider to use") flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])") flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature") - flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjuction with --redis-use-sentinel") - flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjuction with --redis-use-sentinel") + flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel") + flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel") flagSet.String("logging-filename", "", "File to log requests to, empty for stdout") flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation") diff --git a/oauthproxy.go b/oauthproxy.go index 365a14e..f3d9fd9 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -898,7 +898,7 @@ func isAjax(req *http.Request) bool { return false } -// ErrorJSON returns the error code witht an application/json mime type +// ErrorJSON returns the error code with an application/json mime type func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) { rw.Header().Set("Content-Type", applicationJSON) rw.WriteHeader(code)