2016-07-25 20:00:28 +00:00
// Package oidc implements logging in through OpenID Connect providers.
package oidc
2016-08-08 18:45:17 +00:00
import (
2017-03-08 18:33:19 +00:00
"context"
2019-09-25 20:20:19 +00:00
"encoding/json"
2016-08-08 18:45:17 +00:00
"errors"
"fmt"
"net/http"
2017-03-20 15:38:52 +00:00
"net/url"
"strings"
2018-01-29 21:07:15 +00:00
"time"
2016-08-08 18:45:17 +00:00
2021-01-13 18:56:09 +00:00
"github.com/coreos/go-oidc/v3/oidc"
2016-08-08 18:45:17 +00:00
"golang.org/x/oauth2"
2018-09-03 06:44:44 +00:00
"github.com/dexidp/dex/connector"
2019-02-22 12:19:23 +00:00
"github.com/dexidp/dex/pkg/log"
2016-08-08 18:45:17 +00:00
)
// Config holds configuration options for OpenID Connect logins.
type Config struct {
2016-11-03 21:32:23 +00:00
Issuer string ` json:"issuer" `
ClientID string ` json:"clientID" `
ClientSecret string ` json:"clientSecret" `
RedirectURI string ` json:"redirectURI" `
2016-08-08 18:45:17 +00:00
2017-03-20 15:38:52 +00:00
// 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-03 21:32:23 +00:00
Scopes [ ] string ` json:"scopes" ` // defaults to "profile" and "email"
2017-03-20 15:38:52 +00:00
2020-12-20 03:02:04 +00:00
// Override the value of email_verified to true in the returned claims
2019-03-05 21:24:02 +00:00
InsecureSkipEmailVerified bool ` json:"insecureSkipEmailVerified" `
2019-04-24 20:58:35 +00:00
2019-09-12 23:12:29 +00:00
// 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 13:49:44 +00:00
// 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-24 20:58:35 +00:00
// 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 03:51:42 +00:00
UserIDKey string ` json:"userIDKey" `
2019-06-02 19:33:53 +00:00
UserNameKey string ` json:"userNameKey" `
2020-02-19 14:10:28 +00:00
// PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent)
PromptType string ` json:"promptType" `
2020-08-11 20:25:21 +00:00
2021-08-13 10:49:24 +00:00
// 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 08:02:55 +00:00
ClaimMapping struct {
// Configurable key which contains the preferred username claims
PreferredUsernameKey string ` json:"preferred_username" ` // defaults to "preferred_username"
2020-08-11 20:25:21 +00:00
2021-08-19 08:02:55 +00:00
// Configurable key which contains the email claims
EmailKey string ` json:"email" ` // defaults to "email"
2020-08-11 20:25:21 +00:00
2021-08-19 08:02:55 +00:00
// Configurable key which contains the groups claims
GroupsKey string ` json:"groups" ` // defaults to "groups"
} ` json:"claimMapping" `
2017-03-20 15:38:52 +00:00
}
// 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 06:44:44 +00:00
// See: https://github.com/dexidp/dex/issues/859
2017-03-20 15:38:52 +00:00
"okta.com" ,
"oktapreview.com" ,
}
2019-09-25 20:20:19 +00:00
// connectorData stores information for sessions authenticated by this connector
type connectorData struct {
2019-10-02 12:39:52 +00:00
RefreshToken [ ] byte
2019-09-25 20:20:19 +00:00
}
2017-03-20 15:38:52 +00:00
// 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-08 18:45:17 +00:00
// Open returns a connector which can be used to login users through an upstream
// OpenID Connect provider.
2019-02-22 12:19:23 +00:00
func ( c * Config ) Open ( id string , logger log . Logger ) ( conn connector . Connector , err error ) {
2016-08-08 18:45:17 +00:00
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 00:31:22 +00:00
endpoint := provider . Endpoint ( )
2017-03-20 15:38:52 +00:00
if c . BasicAuthUnsupported != nil {
// Setting "basicAuthUnsupported" always overrides our detection.
if * c . BasicAuthUnsupported {
2019-11-16 00:31:22 +00:00
endpoint . AuthStyle = oauth2 . AuthStyleInParams
2017-03-20 15:38:52 +00:00
}
} else if knownBrokenAuthHeaderProvider ( c . Issuer ) {
2019-11-16 00:31:22 +00:00
endpoint . AuthStyle = oauth2 . AuthStyleInParams
2017-03-20 15:38:52 +00:00
}
2016-08-08 18:45:17 +00:00
scopes := [ ] string { oidc . ScopeOpenID }
if len ( c . Scopes ) > 0 {
scopes = append ( scopes , c . Scopes ... )
} else {
scopes = append ( scopes , "profile" , "email" )
}
2020-02-19 14:10:28 +00:00
// PromptType should be "consent" by default, if not set
if c . PromptType == "" {
c . PromptType = "consent"
}
2016-10-22 20:36:31 +00:00
clientID := c . ClientID
2016-08-08 18:45:17 +00:00
return & oidcConnector {
2019-04-24 20:58:35 +00:00
provider : provider ,
2016-08-08 18:45:17 +00:00
redirectURI : c . RedirectURI ,
oauth2Config : & oauth2 . Config {
ClientID : clientID ,
2016-10-22 20:36:31 +00:00
ClientSecret : c . ClientSecret ,
2019-11-16 00:31:22 +00:00
Endpoint : endpoint ,
2016-08-08 18:45:17 +00:00
Scopes : scopes ,
RedirectURL : c . RedirectURI ,
} ,
2016-11-17 23:20:41 +00:00
verifier : provider . Verifier (
2017-03-08 18:33:19 +00:00
& oidc . Config { ClientID : clientID } ,
2016-08-08 18:45:17 +00:00
) ,
2019-03-05 21:24:02 +00:00
logger : logger ,
cancel : cancel ,
insecureSkipEmailVerified : c . InsecureSkipEmailVerified ,
2019-09-12 23:12:29 +00:00
insecureEnableGroups : c . InsecureEnableGroups ,
2022-02-22 13:49:44 +00:00
acrValues : c . AcrValues ,
2019-04-24 20:58:35 +00:00
getUserInfo : c . GetUserInfo ,
2020-02-19 14:10:28 +00:00
promptType : c . PromptType ,
2020-09-08 17:12:53 +00:00
userIDKey : c . UserIDKey ,
userNameKey : c . UserNameKey ,
2021-08-13 10:49:24 +00:00
overrideClaimMapping : c . OverrideClaimMapping ,
2021-08-19 08:02:55 +00:00
preferredUsernameKey : c . ClaimMapping . PreferredUsernameKey ,
emailKey : c . ClaimMapping . EmailKey ,
groupsKey : c . ClaimMapping . GroupsKey ,
2016-08-08 18:45:17 +00:00
} , nil
}
var (
_ connector . CallbackConnector = ( * oidcConnector ) ( nil )
2017-03-23 21:06:30 +00:00
_ connector . RefreshConnector = ( * oidcConnector ) ( nil )
2016-08-08 18:45:17 +00:00
)
type oidcConnector struct {
2019-04-24 20:58:35 +00:00
provider * oidc . Provider
2019-03-05 21:24:02 +00:00
redirectURI string
oauth2Config * oauth2 . Config
verifier * oidc . IDTokenVerifier
cancel context . CancelFunc
logger log . Logger
insecureSkipEmailVerified bool
2019-09-12 23:12:29 +00:00
insecureEnableGroups bool
2022-02-22 13:49:44 +00:00
acrValues [ ] string
2019-04-24 20:58:35 +00:00
getUserInfo bool
2020-08-11 20:25:21 +00:00
promptType string
2019-05-24 03:51:42 +00:00
userIDKey string
2019-06-02 19:33:53 +00:00
userNameKey string
2021-08-13 10:49:24 +00:00
overrideClaimMapping bool
2021-08-19 08:02:55 +00:00
preferredUsernameKey string
emailKey string
groupsKey string
2016-08-08 18:45:17 +00:00
}
func ( c * oidcConnector ) Close ( ) error {
c . cancel ( )
return nil
}
2016-11-18 21:40:41 +00:00
func ( c * oidcConnector ) LoginURL ( s connector . Scopes , callbackURL , state string ) ( string , error ) {
2016-08-08 18:45:17 +00:00
if c . redirectURI != callbackURL {
2017-06-13 22:52:33 +00:00
return "" , fmt . Errorf ( "expected callback URL %q did not match the URL in the config %q" , callbackURL , c . redirectURI )
2016-08-08 18:45:17 +00:00
}
2017-06-21 05:47:28 +00:00
2018-02-04 17:20:05 +00:00
var opts [ ] oauth2 . AuthCodeOption
2022-02-22 13:49:44 +00:00
if len ( c . acrValues ) > 0 {
acrValues := strings . Join ( c . acrValues , " " )
opts = append ( opts , oauth2 . SetAuthURLParam ( "acr_values" , acrValues ) )
}
2018-02-04 17:20:05 +00:00
if s . OfflineAccess {
2020-02-19 14:10:28 +00:00
opts = append ( opts , oauth2 . AccessTypeOffline , oauth2 . SetAuthURLParam ( "prompt" , c . promptType ) )
2018-02-04 17:20:05 +00:00
}
return c . oauth2Config . AuthCodeURL ( state , opts ... ) , nil
2016-08-08 18:45:17 +00:00
}
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 04:13:10 +00:00
type caller uint
const (
createCaller caller = iota
refreshCaller
)
2016-11-18 21:40:41 +00:00
func ( c * oidcConnector ) HandleCallback ( s connector . Scopes , r * http . Request ) ( identity connector . Identity , err error ) {
2016-08-08 18:45:17 +00:00
q := r . URL . Query ( )
if errType := q . Get ( "error" ) ; errType != "" {
2016-10-27 17:08:08 +00:00
return identity , & oauth2Error { errType , q . Get ( "error_description" ) }
2016-08-08 18:45:17 +00:00
}
2016-11-17 23:20:41 +00:00
token , err := c . oauth2Config . Exchange ( r . Context ( ) , q . Get ( "code" ) )
2016-08-08 18:45:17 +00:00
if err != nil {
2016-10-27 17:08:08 +00:00
return identity , fmt . Errorf ( "oidc: failed to get token: %v" , err )
2016-08-08 18:45:17 +00:00
}
2022-05-20 04:13:10 +00:00
return c . createIdentity ( r . Context ( ) , identity , token , createCaller )
2018-02-05 20:58:59 +00:00
}
2019-05-10 14:31:50 +00:00
// Refresh is used to refresh a session with the refresh token provided by the IdP
2018-02-05 20:58:59 +00:00
func ( c * oidcConnector ) Refresh ( ctx context . Context , s connector . Scopes , identity connector . Identity ) ( connector . Identity , error ) {
2019-09-25 20:20:19 +00:00
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-05 20:58:59 +00:00
t := & oauth2 . Token {
2019-10-02 12:39:52 +00:00
RefreshToken : string ( cd . RefreshToken ) ,
2018-02-05 20:58:59 +00:00
Expiry : time . Now ( ) . Add ( - time . Hour ) ,
}
token , err := c . oauth2Config . TokenSource ( ctx , t ) . Token ( )
if err != nil {
2019-09-25 20:12:20 +00:00
return identity , fmt . Errorf ( "oidc: failed to get refresh token: %v" , err )
2018-02-05 20:58:59 +00:00
}
2022-05-20 04:13:10 +00:00
return c . createIdentity ( ctx , identity , token , refreshCaller )
2018-02-05 20:58:59 +00:00
}
2022-05-20 04:13:10 +00:00
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-08 18:45:17 +00:00
rawIDToken , ok := token . Extra ( "id_token" ) . ( string )
2022-05-20 04:13:10 +00:00
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-08 18:45:17 +00:00
2022-05-20 04:13:10 +00:00
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-08 18:45:17 +00:00
}
2019-09-13 18:10:44 +00:00
// We immediately want to run getUserInfo if configured before we validate the claims
if c . getUserInfo {
2018-02-05 20:58:59 +00:00
userInfo , err := c . provider . UserInfo ( ctx , oauth2 . StaticTokenSource ( token ) )
2019-09-13 18:10:44 +00:00
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 04:13:10 +00:00
const subjectClaimKey = "sub"
subject , found := claims [ subjectClaimKey ] . ( string )
if ! found {
2022-05-20 04:35:05 +00:00
return identity , fmt . Errorf ( "missing \"%s\" claim" , subjectClaimKey )
2022-05-20 04:13:10 +00:00
}
2019-06-02 19:33:53 +00:00
userNameKey := "name"
if c . userNameKey != "" {
userNameKey = c . userNameKey
}
name , found := claims [ userNameKey ] . ( string )
2019-05-24 03:51:42 +00:00
if ! found {
2019-06-02 19:33:53 +00:00
return identity , fmt . Errorf ( "missing \"%s\" claim" , userNameKey )
2019-05-24 03:51:42 +00:00
}
2019-12-28 08:18:51 +00:00
2021-08-19 11:28:32 +00:00
preferredUsername , found := claims [ "preferred_username" ] . ( string )
2021-08-19 08:02:55 +00:00
if ( ! found || c . overrideClaimMapping ) && c . preferredUsernameKey != "" {
2021-08-19 11:28:32 +00:00
preferredUsername , _ = claims [ c . preferredUsernameKey ] . ( string )
2020-08-11 20:25:21 +00:00
}
2019-12-28 08:18:51 +00:00
hasEmailScope := false
for _ , s := range c . oauth2Config . Scopes {
if s == "email" {
hasEmailScope = true
break
}
}
2020-08-11 20:25:21 +00:00
var email string
emailKey := "email"
email , found = claims [ emailKey ] . ( string )
2021-08-19 08:02:55 +00:00
if ( ! found || c . overrideClaimMapping ) && c . emailKey != "" {
emailKey = c . emailKey
2020-08-11 20:25:21 +00:00
email , found = claims [ emailKey ] . ( string )
}
2019-12-28 08:18:51 +00:00
if ! found && hasEmailScope {
2020-09-08 14:03:52 +00:00
return identity , fmt . Errorf ( "missing email claim, not found \"%s\" key" , emailKey )
2019-05-24 03:51:42 +00:00
}
2019-12-28 08:18:51 +00:00
2019-05-24 03:51:42 +00:00
emailVerified , found := claims [ "email_verified" ] . ( bool )
if ! found {
2019-05-28 12:43:00 +00:00
if c . insecureSkipEmailVerified {
emailVerified = true
2019-12-28 08:18:51 +00:00
} else if hasEmailScope {
2019-05-28 12:43:00 +00:00
return identity , errors . New ( "missing \"email_verified\" claim" )
}
2019-05-24 03:51:42 +00:00
}
2020-08-11 20:25:21 +00:00
var groups [ ] string
if c . insecureEnableGroups {
groupsKey := "groups"
vs , found := claims [ groupsKey ] . ( [ ] interface { } )
2021-08-19 08:02:55 +00:00
if ( ! found || c . overrideClaimMapping ) && c . groupsKey != "" {
groupsKey = c . groupsKey
2020-08-11 20:25:21 +00:00
vs , found = claims [ groupsKey ] . ( [ ] interface { } )
}
2022-08-25 08:55:30 +00:00
// Fallback when claims[groupsKey] is a string instead of an array of strings.
if g , b := claims [ groupsKey ] . ( string ) ; b {
groups = [ ] string { g }
}
2020-08-11 20:25:21 +00:00
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-27 20:12:11 +00:00
}
2019-09-25 20:20:19 +00:00
cd := connectorData {
2019-10-02 12:39:52 +00:00
RefreshToken : [ ] byte ( token . RefreshToken ) ,
2019-09-25 20:20:19 +00:00
}
connData , err := json . Marshal ( & cd )
if err != nil {
return identity , fmt . Errorf ( "oidc: failed to encode connector data: %v" , err )
}
2016-08-08 18:45:17 +00:00
identity = connector . Identity {
2022-05-20 04:13:10 +00:00
UserID : subject ,
2019-02-27 20:12:11 +00:00
Username : name ,
2020-01-21 16:12:35 +00:00
PreferredUsername : preferredUsername ,
2019-02-27 20:12:11 +00:00
Email : email ,
EmailVerified : emailVerified ,
2020-08-11 20:25:21 +00:00
Groups : groups ,
2019-02-27 20:12:11 +00:00
ConnectorData : connData ,
2019-05-24 03:51:42 +00:00
}
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-08 18:45:17 +00:00
}
2019-05-24 03:51:42 +00:00
2016-10-27 17:08:08 +00:00
return identity , nil
2016-08-08 18:45:17 +00:00
}