1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-08-08 22:46:33 +02:00

extract email from id_token for azure provider (#914)

* extract email from id_token for azure provider

this change fixes a bug when --resource is specified with non-Graph
api and the access token destined to --resource is used to call Graph
api

* fixed typo

* refactor GetEmailAddress to EnrichSessionState

* make getting email from idtoken best effort and fall back to previous behavior when it's absent

* refactor to use jwt package to extract claims

* fix lint

* refactor unit tests to use test table
refactor the get email logic from profile api

* addressing feedback

* added oidc verifier to azure provider and extract email from id_token if present

* fix lint and codeclimate

* refactor to use oidc verifier to verify id_token if oidc is configured

* fixed UT

* addressed comments

* minor refactor

* addressed feedback

* extract email from id_token first and fallback to access token

* fallback to access token as well when id_token doesn't have email claim

* address feedbacks

* updated change log!
This commit is contained in:
Weinong Wang
2021-03-09 20:53:15 -08:00
committed by GitHub
parent 6894738d97
commit f3209a40e1
3 changed files with 355 additions and 155 deletions

View File

@ -107,26 +107,22 @@ func overrideTenantURL(current, defaultURL *url.URL, tenant, path string) {
}
}
func (p *AzureProvider) GetLoginURL(redirectURI, state string) string {
extraParams := url.Values{}
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
extraParams.Add("resource", p.ProtectedResource.String())
}
a := makeLoginURL(p.ProviderData, redirectURI, state, extraParams)
return a.String()
}
// Redeem exchanges the OAuth2 authentication token for an ID token
func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (*sessions.SessionState, error) {
if code == "" {
return nil, ErrMissingCode
}
clientSecret, err := p.GetClientSecret()
params, err := p.prepareRedeem(redirectURL, code)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("redirect_uri", redirectURL)
params.Add("client_id", p.ClientID)
params.Add("client_secret", clientSecret)
params.Add("code", code)
params.Add("grant_type", "authorization_code")
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
params.Add("resource", p.ProtectedResource.String())
}
// blindly try json and x-www-form-urlencoded
var jsonResponse struct {
AccessToken string `json:"access_token"`
@ -149,13 +145,98 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (*
created := time.Now()
expires := time.Unix(jsonResponse.ExpiresOn, 0)
return &sessions.SessionState{
session := &sessions.SessionState{
AccessToken: jsonResponse.AccessToken,
IDToken: jsonResponse.IDToken,
CreatedAt: &created,
ExpiresOn: &expires,
RefreshToken: jsonResponse.RefreshToken,
}, nil
}
email, err := p.verifyTokenAndExtractEmail(ctx, session.IDToken)
// https://github.com/oauth2-proxy/oauth2-proxy/pull/914#issuecomment-782285814
// https://github.com/AzureAD/azure-activedirectory-library-for-java/issues/117
// due to above issues, id_token may not be signed by AAD
// in that case, we will fallback to access token
if err == nil && email != "" {
session.Email = email
} else {
logger.Printf("unable to get email claim from id_token: %v", err)
}
if session.Email == "" {
email, err = p.verifyTokenAndExtractEmail(ctx, session.AccessToken)
if err == nil && email != "" {
session.Email = email
} else {
logger.Printf("unable to get email claim from access token: %v", err)
}
}
return session, nil
}
// EnrichSession finds the email to enrich the session state
func (p *AzureProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
if s.Email != "" {
return nil
}
email, err := p.getEmailFromProfileAPI(ctx, s.AccessToken)
if err != nil {
return fmt.Errorf("unable to get email address: %v", err)
}
if email == "" {
return errors.New("unable to get email address")
}
s.Email = email
return nil
}
func (p *AzureProvider) prepareRedeem(redirectURL, code string) (url.Values, error) {
params := url.Values{}
if code == "" {
return params, ErrMissingCode
}
clientSecret, err := p.GetClientSecret()
if err != nil {
return params, err
}
params.Add("redirect_uri", redirectURL)
params.Add("client_id", p.ClientID)
params.Add("client_secret", clientSecret)
params.Add("code", code)
params.Add("grant_type", "authorization_code")
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
params.Add("resource", p.ProtectedResource.String())
}
return params, nil
}
// verifyTokenAndExtractEmail tries to extract email claim from either id_token or access token
// when oidc verifier is configured
func (p *AzureProvider) verifyTokenAndExtractEmail(ctx context.Context, token string) (string, error) {
email := ""
if token != "" && p.Verifier != nil {
token, err := p.Verifier.Verify(ctx, token)
// due to issues mentioned above, id_token may not be signed by AAD
if err == nil {
claims, err := p.getClaims(token)
if err == nil {
email = claims.Email
} else {
logger.Printf("unable to get claims from token: %v", err)
}
} else {
logger.Printf("unable to verify token: %v", err)
}
}
return email, nil
}
// RefreshSessionIfNeeded checks if the session has expired and uses the
@ -209,6 +290,28 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
s.RefreshToken = jsonResponse.RefreshToken
s.CreatedAt = &now
s.ExpiresOn = &expires
email, err := p.verifyTokenAndExtractEmail(ctx, s.IDToken)
// https://github.com/oauth2-proxy/oauth2-proxy/pull/914#issuecomment-782285814
// https://github.com/AzureAD/azure-activedirectory-library-for-java/issues/117
// due to above issues, id_token may not be signed by AAD
// in that case, we will fallback to access token
if err == nil && email != "" {
s.Email = email
} else {
logger.Printf("unable to get email claim from id_token: %v", err)
}
if s.Email == "" {
email, err = p.verifyTokenAndExtractEmail(ctx, s.AccessToken)
if err == nil && email != "" {
s.Email = email
} else {
logger.Printf("unable to get email claim from access token: %v", err)
}
}
return
}
@ -230,53 +333,32 @@ func getEmailFromJSON(json *simplejson.Json) (string, error) {
err = otherMailsErr
}
if err != nil || email == "" {
email, err = json.Get("userPrincipalName").String()
if err != nil {
logger.Errorf("unable to find userPrincipalName: %s", err)
return "", err
}
}
return email, err
}
// GetEmailAddress returns the Account email address
func (p *AzureProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
var email string
var err error
if s.AccessToken == "" {
func (p *AzureProvider) getEmailFromProfileAPI(ctx context.Context, accessToken string) (string, error) {
if accessToken == "" {
return "", errors.New("missing access token")
}
json, err := requests.New(p.ProfileURL.String()).
WithContext(ctx).
WithHeaders(makeAzureHeader(s.AccessToken)).
WithHeaders(makeAzureHeader(accessToken)).
Do().
UnmarshalJSON()
if err != nil {
return "", err
}
email, err = getEmailFromJSON(json)
if err == nil && email != "" {
return email, err
}
email, err = json.Get("userPrincipalName").String()
if err != nil {
logger.Errorf("failed making request %s", err)
return "", err
}
if email == "" {
logger.Errorf("failed to get email address")
return "", err
}
return email, err
}
func (p *AzureProvider) GetLoginURL(redirectURI, state string) string {
extraParams := url.Values{}
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
extraParams.Add("resource", p.ProtectedResource.String())
}
a := makeLoginURL(p.ProviderData, redirectURI, state, extraParams)
return a.String()
return getEmailFromJSON(json)
}
// ValidateSession validates the AccessToken