2016-07-26 01:30:28 +05:30
// Package oidc implements logging in through OpenID Connect providers.
package oidc
2016-08-09 00:15:17 +05:30
import (
2017-03-09 00:03:19 +05:30
"context"
2019-09-26 01:50:19 +05:30
"encoding/json"
2016-08-09 00:15:17 +05:30
"errors"
"fmt"
"net/http"
2017-03-20 21:08:52 +05:30
"net/url"
"strings"
2018-01-30 02:37:15 +05:30
"time"
2016-08-09 00:15:17 +05:30
2021-01-14 00:26:09 +05:30
"github.com/coreos/go-oidc/v3/oidc"
2016-08-09 00:15:17 +05:30
"golang.org/x/oauth2"
2018-09-03 12:14:44 +05:30
"github.com/dexidp/dex/connector"
2019-02-22 17:49:23 +05:30
"github.com/dexidp/dex/pkg/log"
2016-08-09 00:15:17 +05:30
)
// Config holds configuration options for OpenID Connect logins.
type Config struct {
2016-11-04 03:02:23 +05:30
Issuer string ` json:"issuer" `
ClientID string ` json:"clientID" `
ClientSecret string ` json:"clientSecret" `
RedirectURI string ` json:"redirectURI" `
2016-08-09 00:15:17 +05:30
2017-03-20 21:08:52 +05:30
// Causes client_secret to be passed as POST parameters instead of basic
// auth. This is specifically "NOT RECOMMENDED" by the OAuth2 RFC, but some
// providers require it.
//
// https://tools.ietf.org/html/rfc6749#section-2.3.1
BasicAuthUnsupported * bool ` json:"basicAuthUnsupported" `
2016-11-04 03:02:23 +05:30
Scopes [ ] string ` json:"scopes" ` // defaults to "profile" and "email"
2017-03-20 21:08:52 +05:30
2020-12-20 08:32:04 +05:30
// Override the value of email_verified to true in the returned claims
2019-03-06 02:54:02 +05:30
InsecureSkipEmailVerified bool ` json:"insecureSkipEmailVerified" `
2019-04-25 02:28:35 +05:30
2019-09-13 04:42:29 +05:30
// InsecureEnableGroups enables groups claims. This is disabled by default until https://github.com/dexidp/dex/issues/1065 is resolved
InsecureEnableGroups bool ` json:"insecureEnableGroups" `
2022-02-22 19:19:44 +05:30
// AcrValues (Authentication Context Class Reference Values) that specifies the Authentication Context Class Values
// within the Authentication Request that the Authorization Server is being requested to use for
// processing requests from this Client, with the values appearing in order of preference.
AcrValues [ ] string ` json:"acrValues" `
2019-04-25 02:28:35 +05:30
// GetUserInfo uses the userinfo endpoint to get additional claims for
// the token. This is especially useful where upstreams return "thin"
// id tokens
GetUserInfo bool ` json:"getUserInfo" `
2019-05-24 09:21:42 +05:30
UserIDKey string ` json:"userIDKey" `
2019-06-03 01:03:53 +05:30
UserNameKey string ` json:"userNameKey" `
2020-02-19 19:40:28 +05:30
// PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent)
PromptType string ` json:"promptType" `
2020-08-12 01:55:21 +05:30
2021-08-13 16:19:24 +05:30
// OverrideClaimMapping will be used to override the options defined in claimMappings.
// i.e. if there are 'email' and `preferred_email` claims available, by default Dex will always use the `email` claim independent of the ClaimMapping.EmailKey.
// This setting allows you to override the default behavior of Dex and enforce the mappings defined in `claimMapping`.
OverrideClaimMapping bool ` json:"overrideClaimMapping" ` // defaults to false
2021-08-19 13:32:55 +05:30
ClaimMapping struct {
// Configurable key which contains the preferred username claims
PreferredUsernameKey string ` json:"preferred_username" ` // defaults to "preferred_username"
2020-08-12 01:55:21 +05:30
2021-08-19 13:32:55 +05:30
// Configurable key which contains the email claims
EmailKey string ` json:"email" ` // defaults to "email"
2020-08-12 01:55:21 +05:30
2021-08-19 13:32:55 +05:30
// Configurable key which contains the groups claims
GroupsKey string ` json:"groups" ` // defaults to "groups"
} ` json:"claimMapping" `
2017-03-20 21:08:52 +05:30
}
// Domains that don't support basic auth. golang.org/x/oauth2 has an internal
// list, but it only matches specific URLs, not top level domains.
var brokenAuthHeaderDomains = [ ] string {
2018-09-03 12:14:44 +05:30
// See: https://github.com/dexidp/dex/issues/859
2017-03-20 21:08:52 +05:30
"okta.com" ,
"oktapreview.com" ,
}
2019-09-26 01:50:19 +05:30
// connectorData stores information for sessions authenticated by this connector
type connectorData struct {
2019-10-02 18:09:52 +05:30
RefreshToken [ ] byte
2019-09-26 01:50:19 +05:30
}
2017-03-20 21:08:52 +05:30
// Detect auth header provider issues for known providers. This lets users
// avoid having to explicitly set "basicAuthUnsupported" in their config.
//
// Setting the config field always overrides values returned by this function.
func knownBrokenAuthHeaderProvider ( issuerURL string ) bool {
if u , err := url . Parse ( issuerURL ) ; err == nil {
for _ , host := range brokenAuthHeaderDomains {
if u . Host == host || strings . HasSuffix ( u . Host , "." + host ) {
return true
}
}
}
return false
}
2016-08-09 00:15:17 +05:30
// Open returns a connector which can be used to login users through an upstream
// OpenID Connect provider.
2019-02-22 17:49:23 +05:30
func ( c * Config ) Open ( id string , logger log . Logger ) ( conn connector . Connector , err error ) {
2016-08-09 00:15:17 +05:30
ctx , cancel := context . WithCancel ( context . Background ( ) )
provider , err := oidc . NewProvider ( ctx , c . Issuer )
if err != nil {
cancel ( )
return nil , fmt . Errorf ( "failed to get provider: %v" , err )
}
2019-11-16 06:01:22 +05:30
endpoint := provider . Endpoint ( )
2017-03-20 21:08:52 +05:30
if c . BasicAuthUnsupported != nil {
// Setting "basicAuthUnsupported" always overrides our detection.
if * c . BasicAuthUnsupported {
2019-11-16 06:01:22 +05:30
endpoint . AuthStyle = oauth2 . AuthStyleInParams
2017-03-20 21:08:52 +05:30
}
} else if knownBrokenAuthHeaderProvider ( c . Issuer ) {
2019-11-16 06:01:22 +05:30
endpoint . AuthStyle = oauth2 . AuthStyleInParams
2017-03-20 21:08:52 +05:30
}
2016-08-09 00:15:17 +05:30
scopes := [ ] string { oidc . ScopeOpenID }
if len ( c . Scopes ) > 0 {
scopes = append ( scopes , c . Scopes ... )
} else {
scopes = append ( scopes , "profile" , "email" )
}
2020-02-19 19:40:28 +05:30
// PromptType should be "consent" by default, if not set
if c . PromptType == "" {
c . PromptType = "consent"
}
2016-10-23 02:06:31 +05:30
clientID := c . ClientID
2016-08-09 00:15:17 +05:30
return & oidcConnector {
2019-04-25 02:28:35 +05:30
provider : provider ,
2016-08-09 00:15:17 +05:30
redirectURI : c . RedirectURI ,
oauth2Config : & oauth2 . Config {
ClientID : clientID ,
2016-10-23 02:06:31 +05:30
ClientSecret : c . ClientSecret ,
2019-11-16 06:01:22 +05:30
Endpoint : endpoint ,
2016-08-09 00:15:17 +05:30
Scopes : scopes ,
RedirectURL : c . RedirectURI ,
} ,
2016-11-18 04:50:41 +05:30
verifier : provider . Verifier (
2017-03-09 00:03:19 +05:30
& oidc . Config { ClientID : clientID } ,
2016-08-09 00:15:17 +05:30
) ,
2019-03-06 02:54:02 +05:30
logger : logger ,
cancel : cancel ,
insecureSkipEmailVerified : c . InsecureSkipEmailVerified ,
2019-09-13 04:42:29 +05:30
insecureEnableGroups : c . InsecureEnableGroups ,
2022-02-22 19:19:44 +05:30
acrValues : c . AcrValues ,
2019-04-25 02:28:35 +05:30
getUserInfo : c . GetUserInfo ,
2020-02-19 19:40:28 +05:30
promptType : c . PromptType ,
2020-09-08 22:42:53 +05:30
userIDKey : c . UserIDKey ,
userNameKey : c . UserNameKey ,
2021-08-13 16:19:24 +05:30
overrideClaimMapping : c . OverrideClaimMapping ,
2021-08-19 13:32:55 +05:30
preferredUsernameKey : c . ClaimMapping . PreferredUsernameKey ,
emailKey : c . ClaimMapping . EmailKey ,
groupsKey : c . ClaimMapping . GroupsKey ,
2016-08-09 00:15:17 +05:30
} , nil
}
var (
_ connector . CallbackConnector = ( * oidcConnector ) ( nil )
2017-03-24 02:36:30 +05:30
_ connector . RefreshConnector = ( * oidcConnector ) ( nil )
2016-08-09 00:15:17 +05:30
)
type oidcConnector struct {
2019-04-25 02:28:35 +05:30
provider * oidc . Provider
2019-03-06 02:54:02 +05:30
redirectURI string
oauth2Config * oauth2 . Config
verifier * oidc . IDTokenVerifier
cancel context . CancelFunc
logger log . Logger
insecureSkipEmailVerified bool
2019-09-13 04:42:29 +05:30
insecureEnableGroups bool
2022-02-22 19:19:44 +05:30
acrValues [ ] string
2019-04-25 02:28:35 +05:30
getUserInfo bool
2020-08-12 01:55:21 +05:30
promptType string
2019-05-24 09:21:42 +05:30
userIDKey string
2019-06-03 01:03:53 +05:30
userNameKey string
2021-08-13 16:19:24 +05:30
overrideClaimMapping bool
2021-08-19 13:32:55 +05:30
preferredUsernameKey string
emailKey string
groupsKey string
2016-08-09 00:15:17 +05:30
}
func ( c * oidcConnector ) Close ( ) error {
c . cancel ( )
return nil
}
2016-11-19 03:10:41 +05:30
func ( c * oidcConnector ) LoginURL ( s connector . Scopes , callbackURL , state string ) ( string , error ) {
2016-08-09 00:15:17 +05:30
if c . redirectURI != callbackURL {
2017-06-14 04:22:33 +05:30
return "" , fmt . Errorf ( "expected callback URL %q did not match the URL in the config %q" , callbackURL , c . redirectURI )
2016-08-09 00:15:17 +05:30
}
2017-06-21 11:17:28 +05:30
2018-02-04 22:50:05 +05:30
var opts [ ] oauth2 . AuthCodeOption
2022-02-22 19:19:44 +05:30
if len ( c . acrValues ) > 0 {
acrValues := strings . Join ( c . acrValues , " " )
opts = append ( opts , oauth2 . SetAuthURLParam ( "acr_values" , acrValues ) )
}
2018-02-04 22:50:05 +05:30
if s . OfflineAccess {
2020-02-19 19:40:28 +05:30
opts = append ( opts , oauth2 . AccessTypeOffline , oauth2 . SetAuthURLParam ( "prompt" , c . promptType ) )
2018-02-04 22:50:05 +05:30
}
return c . oauth2Config . AuthCodeURL ( state , opts ... ) , nil
2016-08-09 00:15:17 +05:30
}
type oauth2Error struct {
error string
errorDescription string
}
func ( e * oauth2Error ) Error ( ) string {
if e . errorDescription == "" {
return e . error
}
return e . error + ": " + e . errorDescription
}
2022-05-20 09:43:10 +05:30
type caller uint
const (
createCaller caller = iota
refreshCaller
)
2016-11-19 03:10:41 +05:30
func ( c * oidcConnector ) HandleCallback ( s connector . Scopes , r * http . Request ) ( identity connector . Identity , err error ) {
2016-08-09 00:15:17 +05:30
q := r . URL . Query ( )
if errType := q . Get ( "error" ) ; errType != "" {
2016-10-27 22:38:08 +05:30
return identity , & oauth2Error { errType , q . Get ( "error_description" ) }
2016-08-09 00:15:17 +05:30
}
2016-11-18 04:50:41 +05:30
token , err := c . oauth2Config . Exchange ( r . Context ( ) , q . Get ( "code" ) )
2016-08-09 00:15:17 +05:30
if err != nil {
2016-10-27 22:38:08 +05:30
return identity , fmt . Errorf ( "oidc: failed to get token: %v" , err )
2016-08-09 00:15:17 +05:30
}
2022-05-20 09:43:10 +05:30
return c . createIdentity ( r . Context ( ) , identity , token , createCaller )
2018-02-06 02:28:59 +05:30
}
2019-05-10 20:01:50 +05:30
// Refresh is used to refresh a session with the refresh token provided by the IdP
2018-02-06 02:28:59 +05:30
func ( c * oidcConnector ) Refresh ( ctx context . Context , s connector . Scopes , identity connector . Identity ) ( connector . Identity , error ) {
2019-09-26 01:50:19 +05:30
cd := connectorData { }
err := json . Unmarshal ( identity . ConnectorData , & cd )
if err != nil {
return identity , fmt . Errorf ( "oidc: failed to unmarshal connector data: %v" , err )
}
2018-02-06 02:28:59 +05:30
t := & oauth2 . Token {
2019-10-02 18:09:52 +05:30
RefreshToken : string ( cd . RefreshToken ) ,
2018-02-06 02:28:59 +05:30
Expiry : time . Now ( ) . Add ( - time . Hour ) ,
}
token , err := c . oauth2Config . TokenSource ( ctx , t ) . Token ( )
if err != nil {
2019-09-26 01:42:20 +05:30
return identity , fmt . Errorf ( "oidc: failed to get refresh token: %v" , err )
2018-02-06 02:28:59 +05:30
}
2022-05-20 09:43:10 +05:30
return c . createIdentity ( ctx , identity , token , refreshCaller )
2018-02-06 02:28:59 +05:30
}
2022-05-20 09:43:10 +05:30
func ( c * oidcConnector ) createIdentity ( ctx context . Context , identity connector . Identity , token * oauth2 . Token , caller caller ) ( connector . Identity , error ) {
var claims map [ string ] interface { }
2016-08-09 00:15:17 +05:30
rawIDToken , ok := token . Extra ( "id_token" ) . ( string )
2022-05-20 09:43:10 +05:30
if ok {
idToken , err := c . verifier . Verify ( ctx , rawIDToken )
if err != nil {
return identity , fmt . Errorf ( "oidc: failed to verify ID Token: %v" , err )
}
2016-08-09 00:15:17 +05:30
2022-05-20 09:43:10 +05:30
if err := idToken . Claims ( & claims ) ; err != nil {
return identity , fmt . Errorf ( "oidc: failed to decode claims: %v" , err )
}
} else if caller != refreshCaller {
// ID tokens aren't mandatory in the reply when using a refresh_token grant
return identity , errors . New ( "oidc: no id_token in token response" )
2016-08-09 00:15:17 +05:30
}
2019-09-13 23:40:44 +05:30
// We immediately want to run getUserInfo if configured before we validate the claims
if c . getUserInfo {
2018-02-06 02:28:59 +05:30
userInfo , err := c . provider . UserInfo ( ctx , oauth2 . StaticTokenSource ( token ) )
2019-09-13 23:40:44 +05:30
if err != nil {
return identity , fmt . Errorf ( "oidc: error loading userinfo: %v" , err )
}
if err := userInfo . Claims ( & claims ) ; err != nil {
return identity , fmt . Errorf ( "oidc: failed to decode userinfo claims: %v" , err )
}
}
2022-05-20 09:43:10 +05:30
const subjectClaimKey = "sub"
subject , found := claims [ subjectClaimKey ] . ( string )
if ! found {
return identity , fmt . Errorf ( "missing \"%s\" claim" , subjectClaimKey )
}
2019-06-03 01:03:53 +05:30
userNameKey := "name"
if c . userNameKey != "" {
userNameKey = c . userNameKey
}
name , found := claims [ userNameKey ] . ( string )
2019-05-24 09:21:42 +05:30
if ! found {
2019-06-03 01:03:53 +05:30
return identity , fmt . Errorf ( "missing \"%s\" claim" , userNameKey )
2019-05-24 09:21:42 +05:30
}
2019-12-28 13:48:51 +05:30
2021-08-19 16:58:32 +05:30
preferredUsername , found := claims [ "preferred_username" ] . ( string )
2021-08-19 13:32:55 +05:30
if ( ! found || c . overrideClaimMapping ) && c . preferredUsernameKey != "" {
2021-08-19 16:58:32 +05:30
preferredUsername , _ = claims [ c . preferredUsernameKey ] . ( string )
2020-08-12 01:55:21 +05:30
}
2019-12-28 13:48:51 +05:30
hasEmailScope := false
for _ , s := range c . oauth2Config . Scopes {
if s == "email" {
hasEmailScope = true
break
}
}
2020-08-12 01:55:21 +05:30
var email string
emailKey := "email"
email , found = claims [ emailKey ] . ( string )
2021-08-19 13:32:55 +05:30
if ( ! found || c . overrideClaimMapping ) && c . emailKey != "" {
emailKey = c . emailKey
2020-08-12 01:55:21 +05:30
email , found = claims [ emailKey ] . ( string )
}
2019-12-28 13:48:51 +05:30
if ! found && hasEmailScope {
2020-09-08 19:33:52 +05:30
return identity , fmt . Errorf ( "missing email claim, not found \"%s\" key" , emailKey )
2019-05-24 09:21:42 +05:30
}
2019-12-28 13:48:51 +05:30
2019-05-24 09:21:42 +05:30
emailVerified , found := claims [ "email_verified" ] . ( bool )
if ! found {
2019-05-28 18:13:00 +05:30
if c . insecureSkipEmailVerified {
emailVerified = true
2019-12-28 13:48:51 +05:30
} else if hasEmailScope {
2019-05-28 18:13:00 +05:30
return identity , errors . New ( "missing \"email_verified\" claim" )
}
2019-05-24 09:21:42 +05:30
}
2020-08-12 01:55:21 +05:30
var groups [ ] string
if c . insecureEnableGroups {
groupsKey := "groups"
vs , found := claims [ groupsKey ] . ( [ ] interface { } )
2021-08-19 13:32:55 +05:30
if ( ! found || c . overrideClaimMapping ) && c . groupsKey != "" {
groupsKey = c . groupsKey
2020-08-12 01:55:21 +05:30
vs , found = claims [ groupsKey ] . ( [ ] interface { } )
}
if found {
for _ , v := range vs {
if s , ok := v . ( string ) ; ok {
groups = append ( groups , s )
} else {
return identity , fmt . Errorf ( "malformed \"%v\" claim" , groupsKey )
}
}
}
2019-02-28 01:42:11 +05:30
}
2019-09-26 01:50:19 +05:30
cd := connectorData {
2019-10-02 18:09:52 +05:30
RefreshToken : [ ] byte ( token . RefreshToken ) ,
2019-09-26 01:50:19 +05:30
}
connData , err := json . Marshal ( & cd )
if err != nil {
return identity , fmt . Errorf ( "oidc: failed to encode connector data: %v" , err )
}
2016-08-09 00:15:17 +05:30
identity = connector . Identity {
2022-05-20 09:43:10 +05:30
UserID : subject ,
2019-02-28 01:42:11 +05:30
Username : name ,
2020-01-21 21:42:35 +05:30
PreferredUsername : preferredUsername ,
2019-02-28 01:42:11 +05:30
Email : email ,
EmailVerified : emailVerified ,
2020-08-12 01:55:21 +05:30
Groups : groups ,
2019-02-28 01:42:11 +05:30
ConnectorData : connData ,
2019-05-24 09:21:42 +05:30
}
if c . userIDKey != "" {
userID , found := claims [ c . userIDKey ] . ( string )
if ! found {
return identity , fmt . Errorf ( "oidc: not found %v claim" , c . userIDKey )
}
identity . UserID = userID
2016-08-09 00:15:17 +05:30
}
2019-05-24 09:21:42 +05:30
2016-10-27 22:38:08 +05:30
return identity , nil
2016-08-09 00:15:17 +05:30
}