Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

optionally extract user from id_token in oidc provider #626

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ Usage of oauth2_proxy:
-tls-cert string: path to certificate file
-tls-key string: path to private key file
-upstream value: the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path
-username-claim string: Claim of the id_token that contains the user name when using oidc provider
-validate-url string: Access token validation endpoint
-version: print version string
```
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func main() {
flagSet.String("resource", "", "The resource that is protected (Azure AD only)")
flagSet.String("validate-url", "", "Access token validation endpoint")
flagSet.String("scope", "", "OAuth scope specification")
flagSet.String("username-claim", "", "id_token claim containing the user name")
flagSet.String("approval-prompt", "force", "OAuth approval_prompt")

flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
Expand Down
2 changes: 2 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type Options struct {
ProtectedResource string `flag:"resource" cfg:"resource"`
ValidateURL string `flag:"validate-url" cfg:"validate_url"`
Scope string `flag:"scope" cfg:"scope"`
UsernameClaim string `flag:"username-claim" cfg:"username_claim"`
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`

RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
Expand Down Expand Up @@ -250,6 +251,7 @@ func parseProviderInfo(o *Options, msgs []string) []string {
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ApprovalPrompt: o.ApprovalPrompt,
UsernameClaim: o.UsernameClaim,
}
p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
Expand Down
14 changes: 14 additions & 0 deletions providers/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,25 @@ func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err er
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
}

user := ""
if p.UsernameClaim != "" {
claimsMap := make(map[string]interface{})
if err := idToken.Claims(&claimsMap); err != nil {
return nil, fmt.Errorf("failed to parse id_token claims: %v", err)
}
if u, ok := claimsMap[p.UsernameClaim]; ok {
if user, ok = u.(string); !ok {
return nil, fmt.Errorf("id_token claim %s's value is not a string", p.UsernameClaim)
}
}
}

s = &SessionState{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
ExpiresOn: token.Expiry,
Email: claims.Email,
User: user,
}

return
Expand Down
134 changes: 134 additions & 0 deletions providers/oidc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package providers

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

oidc "github.com/coreos/go-oidc"
"github.com/stretchr/testify/assert"
)

func newJsonReturningRedeemServer(body []byte) (*url.URL, *httptest.Server) {
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header()["Content-Type"] = []string{"application/json"}
rw.Write(body)
}))
u, _ := url.Parse(s.URL)
return u, s
}

var issuer string = "https://exmple.org/"

type testVerifier struct {
}

func (t *testVerifier) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
parts := strings.Split(jwt, ".")
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
return payload, nil
}

func newOidcProvider() *OIDCProvider {
p := NewOIDCProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""})
p.Verifier = oidc.NewVerifier(issuer, &testVerifier{}, &oidc.Config{
SkipClientIDCheck: true,
SkipExpiryCheck: true,
})
return p
}

// reusing redeemResponse from google_test

func TestOidcProviderLeavesUserlankByDefault(t *testing.T) {
p := newOidcProvider()
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
ExpiresIn: 10,
RefreshToken: "refresh12345",
IdToken: base64.RawURLEncoding.EncodeToString([]byte(`{"typ": "jwt", "alg": "none"}`)) + "." + base64.RawURLEncoding.EncodeToString([]byte("{\"iss\": \""+issuer+"\", \"email\": \"[email protected]\", \"email_verified\":true}")) + ".",
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newJsonReturningRedeemServer(body)
defer server.Close()

session, err := p.Redeem("http://redirect/", "code1234")
assert.Nil(t, err)
assert.NotNil(t, session)
assert.Equal(t, "", session.User)
}

func TestOidcProviderLeavesUserBlankIfConfiguredClaimIsMissing(t *testing.T) {
p := newOidcProvider()
p.ProviderData.UsernameClaim = "username"
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
ExpiresIn: 10,
RefreshToken: "refresh12345",
IdToken: base64.RawURLEncoding.EncodeToString([]byte(`{"typ": "jwt", "alg": "none"}`)) + "." + base64.RawURLEncoding.EncodeToString([]byte("{\"iss\": \""+issuer+"\", \"email\": \"[email protected]\", \"email_verified\":true}")) + ".",
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newJsonReturningRedeemServer(body)
defer server.Close()

session, err := p.Redeem("http://redirect/", "code1234")
assert.Nil(t, err)
assert.NotNil(t, session)
assert.Equal(t, "", session.User)
}

func TestOidcProviderSetsUserFromConfiguredClaimIfPresent(t *testing.T) {
p := newOidcProvider()
p.ProviderData.UsernameClaim = "username"
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
ExpiresIn: 10,
RefreshToken: "refresh12345",
IdToken: base64.RawURLEncoding.EncodeToString([]byte(`{"typ": "jwt", "alg": "none"}`)) + "." + base64.RawURLEncoding.EncodeToString([]byte("{\"iss\": \""+issuer+"\", \"email\": \"[email protected]\", \"email_verified\":true, \"username\": \"jd\"}")) + ".",
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newJsonReturningRedeemServer(body)
defer server.Close()

session, err := p.Redeem("http://redirect/", "code1234")
assert.Nil(t, err)
assert.NotNil(t, session)
assert.Equal(t, "jd", session.User)
}

func TestOidcProviderReturnsAnErrorIfConfiguredUsernameClaimIsNotStringValued(t *testing.T) {
p := newOidcProvider()
p.ProviderData.UsernameClaim = "username"
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
ExpiresIn: 10,
RefreshToken: "refresh12345",
IdToken: base64.RawURLEncoding.EncodeToString([]byte(`{"typ": "jwt", "alg": "none"}`)) + "." + base64.RawURLEncoding.EncodeToString([]byte("{\"iss\": \""+issuer+"\", \"email\": \"[email protected]\", \"email_verified\":true, \"username\": true}")) + ".",
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newJsonReturningRedeemServer(body)
defer server.Close()

_, err = p.Redeem("http://redirect/", "code1234")
assert.NotNil(t, err)
}
1 change: 1 addition & 0 deletions providers/provider_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ProviderData struct {
ValidateURL *url.URL
Scope string
ApprovalPrompt string
UsernameClaim string
}

func (p *ProviderData) Data() *ProviderData { return p }