1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-01-26 05:27:28 +02:00
oauth2-proxy/providers/provider_data.go

219 lines
6.3 KiB
Go
Raw Normal View History

package providers
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
2021-07-17 09:55:05 -07:00
"github.com/coreos/go-oidc/v3/oidc"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
improved audience handling to support client credentials access tokens without aud claims (#1204) * implementation draft * add cfg options skip-au-when-missing && client-id-verification-claim; enhance the provider data verification logic for sake of the added options * refactor configs, added logging and add additional claim verification * simplify logic by just having one configuration similar to oidc-email-claim * added internal oidc token verifier, so that aud check behavior can be managed with oauth2-proxy and is compatible with extra-jwt-issuers * refactored verification to reduce complexity * refactored verification to reduce complexity * added docs * adjust tests to support new OIDCAudienceClaim and OIDCExtraAudiences options * extend unit tests and ensure that audience is set with the value of aud claim configuration * revert filemodes and update docs * update docs * remove unneccesary logging, refactor audience existence check and added additional unit tests * fix linting issues after rebase on origin/main * cleanup: use new imports for migrated libraries after rebase on origin/main * adapt mock in keycloak_oidc_test.go * allow specifying multiple audience claims, fixed bug where jwt issuers client id was not the being considered and fixed bug where aud claims with multiple audiences has broken the whole validation * fixed formatting issue * do not pass the whole options struct to minimize complexity and dependency to the configuration structure * added changelog entry * update docs Co-authored-by: Sofia Weiler <sofia.weiler@aoe.com> Co-authored-by: Christian Zenker <christian.zenker@aoe.com>
2022-02-15 17:12:22 +01:00
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/util"
"golang.org/x/oauth2"
)
const (
// This is not exported as it's not currently user configurable
oidcUserClaim = "sub"
)
// ProviderData contains information required to configure all implementations
// of OAuth2 providers
type ProviderData struct {
2015-11-09 09:28:34 +01:00
ProviderName string
LoginURL *url.URL
RedeemURL *url.URL
ProfileURL *url.URL
ProtectedResource *url.URL
ValidateURL *url.URL
// Auth request params & related, see
//https://openid.net/specs/openid-connect-basic-1_0.html#rfc.section.2.1.1.1
AcrValues string
ApprovalPrompt string // NOTE: Renamed to "prompt" in OAuth2
ClientID string
ClientSecret string
ClientSecretFile string
Scope string
Prompt string
// Common OIDC options for any OIDC-based providers to consume
AllowUnverifiedEmail bool
UserClaim string
EmailClaim string
GroupsClaim string
improved audience handling to support client credentials access tokens without aud claims (#1204) * implementation draft * add cfg options skip-au-when-missing && client-id-verification-claim; enhance the provider data verification logic for sake of the added options * refactor configs, added logging and add additional claim verification * simplify logic by just having one configuration similar to oidc-email-claim * added internal oidc token verifier, so that aud check behavior can be managed with oauth2-proxy and is compatible with extra-jwt-issuers * refactored verification to reduce complexity * refactored verification to reduce complexity * added docs * adjust tests to support new OIDCAudienceClaim and OIDCExtraAudiences options * extend unit tests and ensure that audience is set with the value of aud claim configuration * revert filemodes and update docs * update docs * remove unneccesary logging, refactor audience existence check and added additional unit tests * fix linting issues after rebase on origin/main * cleanup: use new imports for migrated libraries after rebase on origin/main * adapt mock in keycloak_oidc_test.go * allow specifying multiple audience claims, fixed bug where jwt issuers client id was not the being considered and fixed bug where aud claims with multiple audiences has broken the whole validation * fixed formatting issue * do not pass the whole options struct to minimize complexity and dependency to the configuration structure * added changelog entry * update docs Co-authored-by: Sofia Weiler <sofia.weiler@aoe.com> Co-authored-by: Christian Zenker <christian.zenker@aoe.com>
2022-02-15 17:12:22 +01:00
Verifier *internaloidc.IDTokenVerifier
// Universal Group authorization data structure
// any provider can set to consume
AllowedGroups map[string]struct{}
getAuthorizationHeaderFunc func(string) http.Header
}
// Data returns the ProviderData
func (p *ProviderData) Data() *ProviderData { return p }
func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
if p.ClientSecret != "" || p.ClientSecretFile == "" {
return p.ClientSecret, nil
}
// Getting ClientSecret can fail in runtime so we need to report it without returning the file name to the user
fileClientSecret, err := ioutil.ReadFile(p.ClientSecretFile)
if err != nil {
logger.Errorf("error reading client secret file %s: %s", p.ClientSecretFile, err)
return "", errors.New("could not read client secret file")
}
return string(fileClientSecret), nil
}
// setAllowedGroups organizes a group list into the AllowedGroups map
// to be consumed by Authorize implementations
func (p *ProviderData) setAllowedGroups(groups []string) {
p.AllowedGroups = make(map[string]struct{}, len(groups))
for _, group := range groups {
p.AllowedGroups[group] = struct{}{}
}
}
type providerDefaults struct {
name string
loginURL *url.URL
redeemURL *url.URL
profileURL *url.URL
validateURL *url.URL
scope string
}
func (p *ProviderData) setProviderDefaults(defaults providerDefaults) {
p.ProviderName = defaults.name
p.LoginURL = defaultURL(p.LoginURL, defaults.loginURL)
p.RedeemURL = defaultURL(p.RedeemURL, defaults.redeemURL)
p.ProfileURL = defaultURL(p.ProfileURL, defaults.profileURL)
p.ValidateURL = defaultURL(p.ValidateURL, defaults.validateURL)
if p.Scope == "" {
p.Scope = defaults.scope
}
if p.UserClaim == "" {
p.UserClaim = oidcUserClaim
}
}
// defaultURL will set return a default value if the given value is not set.
func defaultURL(u *url.URL, d *url.URL) *url.URL {
if u != nil && u.String() != "" {
// The value is already set
return u
}
// If the default is given, return that
if d != nil {
return d
}
return &url.URL{}
}
// ****************************************************************************
// These private OIDC helper methods are available to any providers that are
// OIDC compliant
// ****************************************************************************
func (p *ProviderData) verifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
rawIDToken := getIDToken(token)
2020-12-01 17:50:27 -08:00
if strings.TrimSpace(rawIDToken) == "" {
return nil, ErrMissingIDToken
}
2020-12-01 17:50:27 -08:00
if p.Verifier == nil {
return nil, ErrMissingOIDCVerifier
}
return p.Verifier.Verify(ctx, rawIDToken)
}
// buildSessionFromClaims uses IDToken claims to populate a fresh SessionState
// with non-Token related fields.
func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*sessions.SessionState, error) {
ss := &sessions.SessionState{}
if rawIDToken == "" {
return ss, nil
}
extractor, err := p.getClaimExtractor(rawIDToken, accessToken)
if err != nil {
return nil, err
}
// Use a slice of a struct (vs map) here in case the same claim is used twice
for _, c := range []struct {
claim string
dst interface{}
}{
{p.UserClaim, &ss.User},
{p.EmailClaim, &ss.Email},
{p.GroupsClaim, &ss.Groups},
// TODO (@NickMeves) Deprecate for dynamic claim to session mapping
{"preferred_username", &ss.PreferredUsername},
} {
if _, err := extractor.GetClaimInto(c.claim, c.dst); err != nil {
return nil, err
}
}
// `email_verified` must be present and explicitly set to `false` to be
// considered unverified.
verifyEmail := (p.EmailClaim == options.OIDCEmailClaim) && !p.AllowUnverifiedEmail
var verified bool
exists, err := extractor.GetClaimInto("email_verified", &verified)
if err != nil {
return nil, err
}
if verifyEmail && exists && !verified {
return nil, fmt.Errorf("email in id_token (%s) isn't verified", ss.Email)
}
return ss, nil
}
func (p *ProviderData) getClaimExtractor(rawIDToken, accessToken string) (util.ClaimExtractor, error) {
extractor, err := util.NewClaimExtractor(context.TODO(), rawIDToken, p.ProfileURL, p.getAuthorizationHeader(accessToken))
if err != nil {
return nil, fmt.Errorf("could not initialise claim extractor: %v", err)
}
return extractor, nil
}
// checkNonce compares the session's nonce with the IDToken's nonce claim
func (p *ProviderData) checkNonce(s *sessions.SessionState) error {
extractor, err := p.getClaimExtractor(s.IDToken, "")
if err != nil {
return fmt.Errorf("id_token claims extraction failed: %v", err)
}
var nonce string
if _, err := extractor.GetClaimInto("nonce", &nonce); err != nil {
return fmt.Errorf("could not extract nonce from ID Token: %v", err)
}
if !s.CheckNonce(nonce) {
return errors.New("id_token nonce claim does not match the session nonce")
}
return nil
}
func (p *ProviderData) getAuthorizationHeader(accessToken string) http.Header {
if p.getAuthorizationHeaderFunc != nil && accessToken != "" {
return p.getAuthorizationHeaderFunc(accessToken)
}
return nil
}