This repository has been archived on 2022-08-17. You can view files and clone it, but cannot push or open issues or pull requests.
dex/server/oauth2.go

665 lines
18 KiB
Go
Raw Normal View History

2016-07-26 01:30:28 +05:30
package server
import (
2019-06-20 22:45:59 +05:30
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
2016-07-26 01:30:28 +05:30
"encoding/json"
"errors"
2016-07-26 01:30:28 +05:30
"fmt"
"hash"
"io"
"net"
2016-07-26 01:30:28 +05:30
"net/http"
"net/url"
"strconv"
"strings"
"time"
jose "gopkg.in/square/go-jose.v2"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/server/internal"
"github.com/dexidp/dex/storage"
2016-07-26 01:30:28 +05:30
)
// TODO(ericchiang): clean this file up and figure out more idiomatic error handling.
// authErr is an error response to an authorization request.
// See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
type authErr struct {
State string
RedirectURI string
Type string
Description string
}
func (err *authErr) Status() int {
if err.State == errServerError {
return http.StatusInternalServerError
2016-07-26 01:30:28 +05:30
}
return http.StatusBadRequest
}
func (err *authErr) Error() string {
return err.Description
}
func (err *authErr) Handle() (http.Handler, bool) {
// Didn't get a valid redirect URI.
if err.RedirectURI == "" {
return nil, false
2016-07-26 01:30:28 +05:30
}
hf := func(w http.ResponseWriter, r *http.Request) {
v := url.Values{}
v.Add("state", err.State)
v.Add("error", err.Type)
if err.Description != "" {
v.Add("error_description", err.Description)
}
var redirectURI string
if strings.Contains(err.RedirectURI, "?") {
redirectURI = err.RedirectURI + "&" + v.Encode()
} else {
redirectURI = err.RedirectURI + "?" + v.Encode()
}
http.Redirect(w, r, redirectURI, http.StatusSeeOther)
}
return http.HandlerFunc(hf), true
2016-07-26 01:30:28 +05:30
}
func tokenErr(w http.ResponseWriter, typ, description string, statusCode int) error {
2016-07-26 01:30:28 +05:30
data := struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}{typ, description}
body, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal token error response: %v", err)
2016-07-26 01:30:28 +05:30
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
w.WriteHeader(statusCode)
2016-07-26 01:30:28 +05:30
w.Write(body)
return nil
2016-07-26 01:30:28 +05:30
}
// nolint
2016-07-26 01:30:28 +05:30
const (
errInvalidRequest = "invalid_request"
errUnauthorizedClient = "unauthorized_client"
errAccessDenied = "access_denied"
errUnsupportedResponseType = "unsupported_response_type"
errInvalidScope = "invalid_scope"
errServerError = "server_error"
errTemporarilyUnavailable = "temporarily_unavailable"
errUnsupportedGrantType = "unsupported_grant_type"
errInvalidGrant = "invalid_grant"
errInvalidClient = "invalid_client"
errInvalidConnectorID = "invalid_connector_id"
2016-07-26 01:30:28 +05:30
)
const (
scopeOfflineAccess = "offline_access" // Request a refresh token.
scopeOpenID = "openid"
scopeGroups = "groups"
scopeEmail = "email"
scopeProfile = "profile"
scopeFederatedID = "federated:id"
2016-10-07 23:57:18 +05:30
scopeCrossClientPrefix = "audience:server:client_id:"
2016-07-26 01:30:28 +05:30
)
const (
deviceCallbackURI = "/device/callback"
)
2016-08-20 05:54:49 +05:30
const (
redirectURIOOB = "urn:ietf:wg:oauth:2.0:oob"
)
2016-07-26 01:30:28 +05:30
const (
grantTypeAuthorizationCode = "authorization_code"
2016-07-26 01:30:28 +05:30
grantTypeRefreshToken = "refresh_token"
2018-01-03 08:45:01 +05:30
grantTypePassword = "password"
grantTypeDeviceCode = "urn:ietf:params:oauth:grant-type:device_code"
2016-07-26 01:30:28 +05:30
)
const (
responseTypeCode = "code" // "Regular" flow
responseTypeToken = "token" // Implicit flow for frontend apps.
responseTypeIDToken = "id_token" // ID Token in url fragment
)
const (
deviceTokenPending = "authorization_pending"
deviceTokenComplete = "complete"
deviceTokenSlowDown = "slow_down"
deviceTokenExpired = "expired_token"
)
func parseScopes(scopes []string) connector.Scopes {
var s connector.Scopes
for _, scope := range scopes {
switch scope {
case scopeOfflineAccess:
s.OfflineAccess = true
case scopeGroups:
s.Groups = true
}
}
return s
}
// Determine the signature algorithm for a JWT.
func signatureAlgorithm(jwk *jose.JSONWebKey) (alg jose.SignatureAlgorithm, err error) {
if jwk.Key == nil {
return alg, errors.New("no signing key")
}
switch key := jwk.Key.(type) {
case *rsa.PrivateKey:
// Because OIDC mandates that we support RS256, we always return that
// value. In the future, we might want to make this configurable on a
// per client basis. For example allowing PS256 or ECDSA variants.
//
// See https://github.com/dexidp/dex/issues/692
return jose.RS256, nil
case *ecdsa.PrivateKey:
// We don't actually support ECDSA keys yet, but they're tested for
// in case we want to in the future.
//
// These values are prescribed depending on the ECDSA key type. We
// can't return different values.
switch key.Params() {
case elliptic.P256().Params():
return jose.ES256, nil
case elliptic.P384().Params():
return jose.ES384, nil
case elliptic.P521().Params():
return jose.ES512, nil
default:
return alg, errors.New("unsupported ecdsa curve")
}
default:
return alg, fmt.Errorf("unsupported signing key type %T", key)
}
}
func signPayload(key *jose.JSONWebKey, alg jose.SignatureAlgorithm, payload []byte) (jws string, err error) {
signingKey := jose.SigningKey{Key: key, Algorithm: alg}
signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{})
if err != nil {
return "", fmt.Errorf("new signier: %v", err)
}
signature, err := signer.Sign(payload)
if err != nil {
return "", fmt.Errorf("signing payload: %v", err)
}
return signature.CompactSerialize()
}
2018-10-10 02:21:17 +05:30
// The hash algorithm for the at_hash is determined by the signing
// algorithm used for the id_token. From the spec:
//
// ...the hash algorithm used is the hash algorithm used in the alg Header
// Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256,
// hash the access_token value with SHA-256
//
// https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
var hashForSigAlg = map[jose.SignatureAlgorithm]func() hash.Hash{
jose.RS256: sha256.New,
jose.RS384: sha512.New384,
jose.RS512: sha512.New,
jose.ES256: sha256.New,
jose.ES384: sha512.New384,
jose.ES512: sha512.New,
}
// Compute an at_hash from a raw access token and a signature algorithm
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
func accessTokenHash(alg jose.SignatureAlgorithm, accessToken string) (string, error) {
newHash, ok := hashForSigAlg[alg]
if !ok {
return "", fmt.Errorf("unsupported signature algorithm: %s", alg)
}
hash := newHash()
if _, err := io.WriteString(hash, accessToken); err != nil {
return "", fmt.Errorf("computing hash: %v", err)
}
sum := hash.Sum(nil)
return base64.RawURLEncoding.EncodeToString(sum[:len(sum)/2]), nil
}
2016-07-26 01:30:28 +05:30
type audience []string
func (a audience) contains(aud string) bool {
for _, e := range a {
if aud == e {
return true
}
}
return false
}
2016-07-26 01:30:28 +05:30
func (a audience) MarshalJSON() ([]byte, error) {
if len(a) == 1 {
return json.Marshal(a[0])
}
2016-10-07 23:57:18 +05:30
return json.Marshal([]string(a))
2016-07-26 01:30:28 +05:30
}
type idTokenClaims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience audience `json:"aud"`
Expiry int64 `json:"exp"`
IssuedAt int64 `json:"iat"`
AuthorizingParty string `json:"azp,omitempty"`
Nonce string `json:"nonce,omitempty"`
AccessTokenHash string `json:"at_hash,omitempty"`
2016-07-26 01:30:28 +05:30
Email string `json:"email,omitempty"`
EmailVerified *bool `json:"email_verified,omitempty"`
Groups []string `json:"groups,omitempty"`
Name string `json:"name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
FederatedIDClaims *federatedIDClaims `json:"federated_claims,omitempty"`
}
type federatedIDClaims struct {
ConnectorID string `json:"connector_id,omitempty"`
UserID string `json:"user_id,omitempty"`
2016-07-26 01:30:28 +05:30
}
func (s *Server) newAccessToken(clientID string, claims storage.Claims, scopes []string, nonce, connID string) (accessToken string, err error) {
idToken, _, err := s.newIDToken(clientID, claims, scopes, nonce, storage.NewID(), connID)
return idToken, err
}
func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken, connID string) (idToken string, expiry time.Time, err error) {
keys, err := s.storage.GetKeys()
if err != nil {
s.logger.Errorf("Failed to get keys: %v", err)
return "", expiry, err
}
signingKey := keys.SigningKey
if signingKey == nil {
return "", expiry, fmt.Errorf("no key to sign payload with")
}
signingAlg, err := signatureAlgorithm(signingKey)
if err != nil {
return "", expiry, err
}
2016-07-26 01:30:28 +05:30
issuedAt := s.now()
expiry = issuedAt.Add(s.idTokensValidFor)
sub := &internal.IDTokenSubject{
UserId: claims.UserID,
ConnId: connID,
}
subjectString, err := internal.Marshal(sub)
if err != nil {
s.logger.Errorf("failed to marshal offline session ID: %v", err)
return "", expiry, fmt.Errorf("failed to marshal offline session ID: %v", err)
}
2016-07-26 01:30:28 +05:30
tok := idTokenClaims{
Issuer: s.issuerURL.String(),
Subject: subjectString,
2016-07-26 01:30:28 +05:30
Nonce: nonce,
Expiry: expiry.Unix(),
IssuedAt: issuedAt.Unix(),
}
if accessToken != "" {
atHash, err := accessTokenHash(signingAlg, accessToken)
if err != nil {
s.logger.Errorf("error computing at_hash: %v", err)
return "", expiry, fmt.Errorf("error computing at_hash: %v", err)
}
tok.AccessTokenHash = atHash
}
2016-07-26 01:30:28 +05:30
for _, scope := range scopes {
switch {
case scope == scopeEmail:
tok.Email = claims.Email
tok.EmailVerified = &claims.EmailVerified
case scope == scopeGroups:
tok.Groups = claims.Groups
case scope == scopeProfile:
tok.Name = claims.Username
tok.PreferredUsername = claims.PreferredUsername
case scope == scopeFederatedID:
tok.FederatedIDClaims = &federatedIDClaims{
ConnectorID: connID,
UserID: claims.UserID,
}
2016-07-26 01:30:28 +05:30
default:
peerID, ok := parseCrossClientScope(scope)
if !ok {
// Ignore unknown scopes. These are already validated during the
// initial auth request.
2016-07-26 01:30:28 +05:30
continue
}
isTrusted, err := s.validateCrossClientTrust(clientID, peerID)
2016-07-26 01:30:28 +05:30
if err != nil {
return "", expiry, err
}
if !isTrusted {
// TODO(ericchiang): propagate this error to the client.
return "", expiry, fmt.Errorf("peer (%s) does not trust client", peerID)
}
tok.Audience = append(tok.Audience, peerID)
}
}
2016-07-26 01:30:28 +05:30
if len(tok.Audience) == 0 {
// Client didn't ask for cross client audience. Set the current
// client as the audience.
2016-07-26 01:30:28 +05:30
tok.Audience = audience{clientID}
} else {
// Client asked for cross client audience:
// if the current client was not requested explicitly
if !tok.Audience.contains(clientID) {
// by default it becomes one of entries in Audience
tok.Audience = append(tok.Audience, clientID)
}
// The current client becomes the authorizing party.
2016-07-26 01:30:28 +05:30
tok.AuthorizingParty = clientID
}
payload, err := json.Marshal(tok)
if err != nil {
return "", expiry, fmt.Errorf("could not serialize claims: %v", err)
}
if idToken, err = signPayload(signingKey, signingAlg, payload); err != nil {
2016-07-26 01:30:28 +05:30
return "", expiry, fmt.Errorf("failed to sign payload: %v", err)
}
return idToken, expiry, nil
}
// parse the initial request from the OAuth2 client.
func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthRequest, error) {
if err := r.ParseForm(); err != nil {
return nil, &authErr{"", "", errInvalidRequest, "Failed to parse request body."}
}
q := r.Form
redirectURI, err := url.QueryUnescape(q.Get("redirect_uri"))
2016-07-26 01:30:28 +05:30
if err != nil {
return nil, &authErr{"", "", errInvalidRequest, "No redirect_uri provided."}
2016-07-26 01:30:28 +05:30
}
clientID := q.Get("client_id")
state := q.Get("state")
nonce := q.Get("nonce")
connectorID := q.Get("connector_id")
// Some clients, like the old go-oidc, provide extra whitespace. Tolerate this.
scopes := strings.Fields(q.Get("scope"))
responseTypes := strings.Fields(q.Get("response_type"))
2016-07-26 01:30:28 +05:30
PKCE implementation (#1784) * Basic implementation of PKCE Signed-off-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com> * @mfmarche on 24 Feb: when code_verifier is set, don't check client_secret In PKCE flow, no client_secret is used, so the check for a valid client_secret would always fail. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * @deric on 16 Jun: return invalid_grant when wrong code_verifier Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Enforce PKCE flow on /token when PKCE flow was started on /auth Also dissallow PKCE on /token, when PKCE flow was not started on /auth Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * fixed error messages when mixed PKCE/no PKCE flow. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * server_test.go: Added PKCE error cases on /token endpoint * Added test for invalid_grant, when wrong code_verifier is sent * Added test for mixed PKCE / no PKCE auth flows. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * cleanup: extracted method checkErrorResponse and type TestDefinition * fixed connector being overwritten Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * /token endpoint: skip client_secret verification only for grand type authorization_code with PKCE extension Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Allow "Authorization" header in CORS handlers * Adds "Authorization" to the default CORS headers{"Accept", "Accept-Language", "Content-Language", "Origin"} Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Add "code_challenge_methods_supported" to discovery endpoint discovery endpoint /dex/.well-known/openid-configuration now has the following entry: "code_challenge_methods_supported": [ "S256", "plain" ] Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Updated tests (mixed-up comments), added a PKCE test * @asoorm added test that checks if downgrade to "plain" on /token endpoint Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * remove redefinition of providedCodeVerifier, fixed spelling (#6) Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> Signed-off-by: Bernd Eckstein <HEllRZA@users.noreply.github.com> * Rename struct CodeChallenge to PKCE Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * PKCE: Check clientSecret when available In authorization_code flow with PKCE, allow empty client_secret on /auth and /token endpoints. But check the client_secret when it is given. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Enable PKCE with public: true dex configuration public on staticClients now enables the following behavior in PKCE: - Public: false, PKCE will always check client_secret. This means PKCE in it's natural form is disabled. - Public: true, PKCE is enabled. It will only check client_secret if the client has sent one. But it allows the code flow if the client didn't sent one. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Redirect error on unsupported code_challenge_method - Check for unsupported code_challenge_method after redirect uri is validated, and use newErr() to return the error. - Add PKCE tests to oauth2_test.go Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Reverted go.mod and go.sum to the state of master Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Don't omit client secret check for PKCE Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Allow public clients (e.g. with PKCE) to have redirect URIs configured Signed-off-by: Martin Heide <martin.heide@faro.com> * Remove "Authorization" as Accepted Headers on CORS, small fixes Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Revert "Allow public clients (e.g. with PKCE) to have redirect URIs configured" This reverts commit b6e297b78537dc44cd3e1374f0b4d34bf89404ac. Signed-off-by: Martin Heide <martin.heide@faro.com> * PKCE on client_secret client error message * When connecting to the token endpoint with PKCE without client_secret, but the client is configured with a client_secret, generate a special error message. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Output info message when PKCE without client_secret used on confidential client * removes the special error message Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * General missing/invalid client_secret message on token endpoint Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> Co-authored-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com> Co-authored-by: Martin Heide <martin.heide@faro.com> Co-authored-by: M. Heide <66078329+heidemn-faro@users.noreply.github.com>
2020-10-26 16:03:40 +05:30
codeChallenge := q.Get("code_challenge")
codeChallengeMethod := q.Get("code_challenge_method")
if codeChallengeMethod == "" {
codeChallengeMethod = CodeChallengeMethodPlain
}
client, err := s.storage.GetClient(clientID)
2016-07-26 01:30:28 +05:30
if err != nil {
if err == storage.ErrNotFound {
description := fmt.Sprintf("Invalid client_id (%q).", clientID)
return nil, &authErr{"", "", errUnauthorizedClient, description}
2016-07-26 01:30:28 +05:30
}
s.logger.Errorf("Failed to get client: %v", err)
return nil, &authErr{"", "", errServerError, ""}
2016-07-26 01:30:28 +05:30
}
if connectorID != "" {
connectors, err := s.storage.ListConnectors()
if err != nil {
return nil, &authErr{"", "", errServerError, "Unable to retrieve connectors"}
}
if !validateConnectorID(connectors, connectorID) {
return nil, &authErr{"", "", errInvalidRequest, "Invalid ConnectorID"}
}
}
2016-07-26 01:30:28 +05:30
if !validateRedirectURI(client, redirectURI) {
description := fmt.Sprintf("Unregistered redirect_uri (%q).", redirectURI)
return nil, &authErr{"", "", errInvalidRequest, description}
2016-07-26 01:30:28 +05:30
}
if redirectURI == deviceCallbackURI && client.Public {
redirectURI = s.issuerURL.Path + deviceCallbackURI
}
2016-07-26 01:30:28 +05:30
// From here on out, we want to redirect back to the client with an error.
2016-07-26 01:30:28 +05:30
newErr := func(typ, format string, a ...interface{}) *authErr {
return &authErr{state, redirectURI, typ, fmt.Sprintf(format, a...)}
}
PKCE implementation (#1784) * Basic implementation of PKCE Signed-off-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com> * @mfmarche on 24 Feb: when code_verifier is set, don't check client_secret In PKCE flow, no client_secret is used, so the check for a valid client_secret would always fail. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * @deric on 16 Jun: return invalid_grant when wrong code_verifier Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Enforce PKCE flow on /token when PKCE flow was started on /auth Also dissallow PKCE on /token, when PKCE flow was not started on /auth Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * fixed error messages when mixed PKCE/no PKCE flow. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * server_test.go: Added PKCE error cases on /token endpoint * Added test for invalid_grant, when wrong code_verifier is sent * Added test for mixed PKCE / no PKCE auth flows. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * cleanup: extracted method checkErrorResponse and type TestDefinition * fixed connector being overwritten Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * /token endpoint: skip client_secret verification only for grand type authorization_code with PKCE extension Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Allow "Authorization" header in CORS handlers * Adds "Authorization" to the default CORS headers{"Accept", "Accept-Language", "Content-Language", "Origin"} Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Add "code_challenge_methods_supported" to discovery endpoint discovery endpoint /dex/.well-known/openid-configuration now has the following entry: "code_challenge_methods_supported": [ "S256", "plain" ] Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Updated tests (mixed-up comments), added a PKCE test * @asoorm added test that checks if downgrade to "plain" on /token endpoint Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * remove redefinition of providedCodeVerifier, fixed spelling (#6) Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> Signed-off-by: Bernd Eckstein <HEllRZA@users.noreply.github.com> * Rename struct CodeChallenge to PKCE Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * PKCE: Check clientSecret when available In authorization_code flow with PKCE, allow empty client_secret on /auth and /token endpoints. But check the client_secret when it is given. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Enable PKCE with public: true dex configuration public on staticClients now enables the following behavior in PKCE: - Public: false, PKCE will always check client_secret. This means PKCE in it's natural form is disabled. - Public: true, PKCE is enabled. It will only check client_secret if the client has sent one. But it allows the code flow if the client didn't sent one. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Redirect error on unsupported code_challenge_method - Check for unsupported code_challenge_method after redirect uri is validated, and use newErr() to return the error. - Add PKCE tests to oauth2_test.go Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Reverted go.mod and go.sum to the state of master Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Don't omit client secret check for PKCE Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Allow public clients (e.g. with PKCE) to have redirect URIs configured Signed-off-by: Martin Heide <martin.heide@faro.com> * Remove "Authorization" as Accepted Headers on CORS, small fixes Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Revert "Allow public clients (e.g. with PKCE) to have redirect URIs configured" This reverts commit b6e297b78537dc44cd3e1374f0b4d34bf89404ac. Signed-off-by: Martin Heide <martin.heide@faro.com> * PKCE on client_secret client error message * When connecting to the token endpoint with PKCE without client_secret, but the client is configured with a client_secret, generate a special error message. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Output info message when PKCE without client_secret used on confidential client * removes the special error message Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * General missing/invalid client_secret message on token endpoint Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> Co-authored-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com> Co-authored-by: Martin Heide <martin.heide@faro.com> Co-authored-by: M. Heide <66078329+heidemn-faro@users.noreply.github.com>
2020-10-26 16:03:40 +05:30
if codeChallengeMethod != CodeChallengeMethodS256 && codeChallengeMethod != CodeChallengeMethodPlain {
description := fmt.Sprintf("Unsupported PKCE challenge method (%q).", codeChallengeMethod)
return nil, newErr(errInvalidRequest, description)
}
2016-07-26 01:30:28 +05:30
var (
unrecognized []string
invalidScopes []string
)
hasOpenIDScope := false
for _, scope := range scopes {
switch scope {
case scopeOpenID:
hasOpenIDScope = true
case scopeOfflineAccess, scopeEmail, scopeProfile, scopeGroups, scopeFederatedID:
2016-07-26 01:30:28 +05:30
default:
peerID, ok := parseCrossClientScope(scope)
if !ok {
unrecognized = append(unrecognized, scope)
continue
}
isTrusted, err := s.validateCrossClientTrust(clientID, peerID)
2016-07-26 01:30:28 +05:30
if err != nil {
return nil, newErr(errServerError, "Internal server error.")
2016-07-26 01:30:28 +05:30
}
if !isTrusted {
invalidScopes = append(invalidScopes, scope)
}
}
}
if !hasOpenIDScope {
return nil, newErr("invalid_scope", `Missing required scope(s) ["openid"].`)
2016-07-26 01:30:28 +05:30
}
if len(unrecognized) > 0 {
return nil, newErr("invalid_scope", "Unrecognized scope(s) %q", unrecognized)
2016-07-26 01:30:28 +05:30
}
if len(invalidScopes) > 0 {
return nil, newErr("invalid_scope", "Client can't request scope(s) %q", invalidScopes)
2016-07-26 01:30:28 +05:30
}
var rt struct {
code bool
idToken bool
token bool
}
2016-08-20 05:54:49 +05:30
for _, responseType := range responseTypes {
2016-08-20 05:54:49 +05:30
switch responseType {
case responseTypeCode:
rt.code = true
case responseTypeIDToken:
rt.idToken = true
2016-08-20 05:54:49 +05:30
case responseTypeToken:
rt.token = true
2016-08-20 05:54:49 +05:30
default:
return nil, newErr("invalid_request", "Invalid response type %q", responseType)
2016-07-26 01:30:28 +05:30
}
if !s.supportedResponseTypes[responseType] {
return nil, newErr(errUnsupportedResponseType, "Unsupported response type %q", responseType)
}
}
if len(responseTypes) == 0 {
return nil, newErr("invalid_requests", "No response_type provided")
}
if rt.token && !rt.code && !rt.idToken {
// "token" can't be provided by its own.
//
// https://openid.net/specs/openid-connect-core-1_0.html#Authentication
return nil, newErr("invalid_request", "Response type 'token' must be provided with type 'id_token' and/or 'code'")
}
if !rt.code {
2020-05-21 16:30:53 +05:30
// Either "id_token token" or "id_token" has been provided which implies the
// implicit flow. Implicit flow requires a nonce value.
//
// https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest
if nonce == "" {
return nil, newErr("invalid_request", "Response type 'token' requires a 'nonce' value.")
}
}
if rt.token {
if redirectURI == redirectURIOOB {
err := fmt.Sprintf("Cannot use response type 'token' with redirect_uri '%s'.", redirectURIOOB)
return nil, newErr("invalid_request", err)
}
2016-07-26 01:30:28 +05:30
}
return &storage.AuthRequest{
2016-08-03 10:27:36 +05:30
ID: storage.NewID(),
2016-07-26 01:30:28 +05:30
ClientID: client.ID,
State: state,
2016-08-20 05:54:49 +05:30
Nonce: nonce,
ForceApprovalPrompt: q.Get("approval_prompt") == "force",
2016-07-26 01:30:28 +05:30
Scopes: scopes,
RedirectURI: redirectURI,
ResponseTypes: responseTypes,
ConnectorID: connectorID,
PKCE implementation (#1784) * Basic implementation of PKCE Signed-off-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com> * @mfmarche on 24 Feb: when code_verifier is set, don't check client_secret In PKCE flow, no client_secret is used, so the check for a valid client_secret would always fail. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * @deric on 16 Jun: return invalid_grant when wrong code_verifier Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Enforce PKCE flow on /token when PKCE flow was started on /auth Also dissallow PKCE on /token, when PKCE flow was not started on /auth Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * fixed error messages when mixed PKCE/no PKCE flow. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * server_test.go: Added PKCE error cases on /token endpoint * Added test for invalid_grant, when wrong code_verifier is sent * Added test for mixed PKCE / no PKCE auth flows. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * cleanup: extracted method checkErrorResponse and type TestDefinition * fixed connector being overwritten Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * /token endpoint: skip client_secret verification only for grand type authorization_code with PKCE extension Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Allow "Authorization" header in CORS handlers * Adds "Authorization" to the default CORS headers{"Accept", "Accept-Language", "Content-Language", "Origin"} Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Add "code_challenge_methods_supported" to discovery endpoint discovery endpoint /dex/.well-known/openid-configuration now has the following entry: "code_challenge_methods_supported": [ "S256", "plain" ] Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Updated tests (mixed-up comments), added a PKCE test * @asoorm added test that checks if downgrade to "plain" on /token endpoint Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * remove redefinition of providedCodeVerifier, fixed spelling (#6) Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> Signed-off-by: Bernd Eckstein <HEllRZA@users.noreply.github.com> * Rename struct CodeChallenge to PKCE Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * PKCE: Check clientSecret when available In authorization_code flow with PKCE, allow empty client_secret on /auth and /token endpoints. But check the client_secret when it is given. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Enable PKCE with public: true dex configuration public on staticClients now enables the following behavior in PKCE: - Public: false, PKCE will always check client_secret. This means PKCE in it's natural form is disabled. - Public: true, PKCE is enabled. It will only check client_secret if the client has sent one. But it allows the code flow if the client didn't sent one. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Redirect error on unsupported code_challenge_method - Check for unsupported code_challenge_method after redirect uri is validated, and use newErr() to return the error. - Add PKCE tests to oauth2_test.go Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Reverted go.mod and go.sum to the state of master Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Don't omit client secret check for PKCE Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Allow public clients (e.g. with PKCE) to have redirect URIs configured Signed-off-by: Martin Heide <martin.heide@faro.com> * Remove "Authorization" as Accepted Headers on CORS, small fixes Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Revert "Allow public clients (e.g. with PKCE) to have redirect URIs configured" This reverts commit b6e297b78537dc44cd3e1374f0b4d34bf89404ac. Signed-off-by: Martin Heide <martin.heide@faro.com> * PKCE on client_secret client error message * When connecting to the token endpoint with PKCE without client_secret, but the client is configured with a client_secret, generate a special error message. Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * Output info message when PKCE without client_secret used on confidential client * removes the special error message Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> * General missing/invalid client_secret message on token endpoint Signed-off-by: Bernd Eckstein <Bernd.Eckstein@faro.com> Co-authored-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com> Co-authored-by: Martin Heide <martin.heide@faro.com> Co-authored-by: M. Heide <66078329+heidemn-faro@users.noreply.github.com>
2020-10-26 16:03:40 +05:30
PKCE: storage.PKCE{
CodeChallenge: codeChallenge,
CodeChallengeMethod: codeChallengeMethod,
},
2016-07-26 01:30:28 +05:30
}, nil
}
func parseCrossClientScope(scope string) (peerID string, ok bool) {
if ok = strings.HasPrefix(scope, scopeCrossClientPrefix); ok {
peerID = scope[len(scopeCrossClientPrefix):]
}
return
}
func (s *Server) validateCrossClientTrust(clientID, peerID string) (trusted bool, err error) {
2016-07-26 01:30:28 +05:30
if peerID == clientID {
return true, nil
}
peer, err := s.storage.GetClient(peerID)
2016-07-26 01:30:28 +05:30
if err != nil {
if err != storage.ErrNotFound {
s.logger.Errorf("Failed to get client: %v", err)
2016-07-26 01:30:28 +05:30
return false, err
}
return false, nil
}
for _, id := range peer.TrustedPeers {
if id == clientID {
return true, nil
}
}
return false, nil
}
func validateRedirectURI(client storage.Client, redirectURI string) bool {
if !client.Public {
for _, uri := range client.RedirectURIs {
if redirectURI == uri {
return true
}
}
return false
}
if redirectURI == redirectURIOOB || redirectURI == deviceCallbackURI {
2016-07-26 01:30:28 +05:30
return true
}
// verify that the host is of form "http://localhost:(port)(path)" or "http://localhost(path)"
u, err := url.Parse(redirectURI)
if err != nil {
return false
}
if u.Scheme != "http" {
2016-07-26 01:30:28 +05:30
return false
}
if u.Host == "localhost" {
return true
}
host, _, err := net.SplitHostPort(u.Host)
return err == nil && host == "localhost"
2016-07-26 01:30:28 +05:30
}
2019-06-20 22:45:59 +05:30
func validateConnectorID(connectors []storage.Connector, connectorID string) bool {
for _, c := range connectors {
if c.ID == connectorID {
return true
}
}
return false
}
2019-06-20 22:45:59 +05:30
// storageKeySet implements the oidc.KeySet interface backed by Dex storage
type storageKeySet struct {
storage.Storage
}
2019-06-24 19:13:12 +05:30
func (s *storageKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
2019-06-20 22:45:59 +05:30
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, err
}
keyID := ""
for _, sig := range jws.Signatures {
keyID = sig.Header.KeyID
break
}
skeys, err := s.Storage.GetKeys()
if err != nil {
return nil, err
}
keys := []*jose.JSONWebKey{skeys.SigningKeyPub}
for _, vk := range skeys.VerificationKeys {
keys = append(keys, vk.PublicKey)
}
for _, key := range keys {
if keyID == "" || key.KeyID == keyID {
if payload, err := jws.Verify(key); err == nil {
return payload, nil
}
}
}
return nil, errors.New("failed to verify id token signature")
}