Newer
Older
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/server/internal"
"github.com/dexidp/dex/storage"
codeChallengeMethodPlain = "plain"
codeChallengeMethodS256 = "S256"
func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
s.logger.ErrorContext(r.Context(), "failed to get keys", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
s.logger.ErrorContext(r.Context(), "no public keys found.")
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return
}
jwks := jose.JSONWebKeySet{
Keys: make([]jose.JSONWebKey, len(keys.VerificationKeys)+1),
}
jwks.Keys[0] = *keys.SigningKeyPub
for i, verificationKey := range keys.VerificationKeys {
jwks.Keys[i+1] = *verificationKey.PublicKey
}
data, err := json.MarshalIndent(jwks, "", " ")
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to marshal discovery data", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return
}
maxAge := keys.NextRotation.Sub(s.now())
if maxAge < (time.Minute * 2) {
maxAge = time.Minute * 2
}
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, must-revalidate", int(maxAge.Seconds())))
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Write(data)
}
type discovery struct {
Issuer string `json:"issuer"`
Auth string `json:"authorization_endpoint"`
Token string `json:"token_endpoint"`
Keys string `json:"jwks_uri"`
UserInfo string `json:"userinfo_endpoint"`
DeviceEndpoint string `json:"device_authorization_endpoint"`
Romain Caire
committed
Introspect string `json:"introspection_endpoint"`
GrantTypes []string `json:"grant_types_supported"`
ResponseTypes []string `json:"response_types_supported"`
Subjects []string `json:"subject_types_supported"`
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
CodeChallengeAlgs []string `json:"code_challenge_methods_supported"`
Scopes []string `json:"scopes_supported"`
AuthMethods []string `json:"token_endpoint_auth_methods_supported"`
Claims []string `json:"claims_supported"`
func (s *Server) discoveryHandler() (http.HandlerFunc, error) {
d := s.constructDiscovery()
data, err := json.MarshalIndent(d, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal discovery data: %v", err)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Write(data)
}), nil
}
func (s *Server) constructDiscovery() discovery {
Issuer: s.issuerURL.String(),
Auth: s.absURL("/auth"),
Token: s.absURL("/token"),
Keys: s.absURL("/keys"),
UserInfo: s.absURL("/userinfo"),
DeviceEndpoint: s.absURL("/device/code"),
Romain Caire
committed
Introspect: s.absURL("/token/introspect"),
Subjects: []string{"public"},
IDTokenAlgs: []string{string(jose.RS256)},
CodeChallengeAlgs: []string{codeChallengeMethodS256, codeChallengeMethodPlain},
Scopes: []string{"openid", "email", "groups", "profile", "offline_access"},
AuthMethods: []string{"client_secret_basic", "client_secret_post"},
"iss", "sub", "aud", "iat", "exp", "email", "email_verified",
"locale", "name", "preferred_username", "at_hash",
for responseType := range s.supportedResponseTypes {
d.ResponseTypes = append(d.ResponseTypes, responseType)
}
sort.Strings(d.ResponseTypes)
d.GrantTypes = s.supportedGrantTypes
}
// handleAuthorization handles the OAuth2 auth endpoint.
func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
// Extract the arguments
if err := r.ParseForm(); err != nil {
s.logger.ErrorContext(r.Context(), "failed to parse arguments", "err", err)
s.renderError(r, w, http.StatusBadRequest, err.Error())
connectorID := r.Form.Get("connector_id")
connectors, err := s.storage.ListConnectors(ctx)
s.logger.ErrorContext(r.Context(), "failed to get list of connectors", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve connector list.")
return
}
// We don't need connector_id any more
r.Form.Del("connector_id")
// Construct a URL with all of the arguments in its query
connURL := url.URL{
RawQuery: r.Form.Encode(),
}
// Redirect if a client chooses a specific connector_id
for _, c := range connectors {
connURL.Path = s.absPath("/auth", url.PathEscape(c.ID))
http.Redirect(w, r, connURL.String(), http.StatusFound)
return
}
}
s.renderError(r, w, http.StatusBadRequest, "Connector ID does not match a valid Connector")
return
}
Marc-André Dufresne
committed
if len(connectors) == 1 && !s.alwaysShowLogin {
connURL.Path = s.absPath("/auth", url.PathEscape(connectors[0].ID))
http.Redirect(w, r, connURL.String(), http.StatusFound)
connectorInfos := make([]connectorInfo, len(connectors))
for index, conn := range connectors {
connURL.Path = s.absPath("/auth", url.PathEscape(conn.ID))
connectorInfos[index] = connectorInfo{
ID: conn.ID,
Name: conn.Name,
Type: conn.Type,
URL: template.URL(connURL.String()),
if err := s.templates.login(r, w, connectorInfos); err != nil {
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
}
func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
authReq, err := s.parseAuthorizationRequest(r)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to parse authorization request", "err", err)
switch authErr := err.(type) {
case *redirectedAuthErr:
authErr.Handler().ServeHTTP(w, r)
case *displayedAuthErr:
s.renderError(r, w, authErr.Status, err.Error())
default:
panic("unsupported error type")
connID, err := url.PathUnescape(mux.Vars(r)["connector"])
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to parse connector", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
return
}
if err != nil {
s.logger.ErrorContext(r.Context(), "Failed to get connector", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
// Set the connector being used for the login.
if authReq.ConnectorID != "" && authReq.ConnectorID != connID {
s.logger.ErrorContext(r.Context(), "mismatched connector ID in auth request",
"auth_request_connector_id", authReq.ConnectorID, "connector_id", connID)
s.renderError(r, w, http.StatusBadRequest, "Bad connector ID")
authReq.ConnectorID = connID
// Actually create the auth request
authReq.Expiry = s.now().Add(s.authRequestsValidFor)
if err := s.storage.CreateAuthRequest(ctx, *authReq); err != nil {
s.logger.ErrorContext(r.Context(), "failed to create authorization request", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Failed to connect to the database.")
return
}
scopes := parseScopes(authReq.Scopes)
// Work out where the "Select another login method" link should go.
backLink := ""
if len(s.connectors) > 1 {
backLinkURL := url.URL{
Path: s.absPath("/auth"),
RawQuery: r.Form.Encode(),
}
backLink = backLinkURL.String()
}
case http.MethodGet:
switch conn := conn.Connector.(type) {
case connector.CallbackConnector:
// Use the auth request ID as the "state" token.
//
// TODO(ericchiang): Is this appropriate or should we also be using a nonce?
callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReq.ID)
s.logger.ErrorContext(r.Context(), "connector returned error when creating callback", "connector_id", connID, "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return
}
http.Redirect(w, r, callbackURL, http.StatusFound)
case connector.PasswordConnector:
loginURL := url.URL{
Path: s.absPath("/auth", connID, "login"),
q := loginURL.Query()
q.Set("state", authReq.ID)
q.Set("back", backLink)
loginURL.RawQuery = q.Encode()
http.Redirect(w, r, loginURL.String(), http.StatusFound)
action, value, err := conn.POSTData(scopes, authReq.ID)
s.logger.ErrorContext(r.Context(), "creating SAML data", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Connector Login Error")
return
}
// TODO(ericchiang): Don't inline this.
fmt.Fprintf(w, `<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>SAML login</title>
</head>
<body>
<form method="post" action="%s" >
<input type="hidden" name="SAMLRequest" value="%s" />
<input type="hidden" name="RelayState" value="%s" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>`, action, value, authReq.ID)
s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
default:
s.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
}
}
func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
authID := r.URL.Query().Get("state")
if authID == "" {
s.renderError(r, w, http.StatusBadRequest, "User session error.")
return
}
backLink := r.URL.Query().Get("back")
authReq, err := s.storage.GetAuthRequest(ctx, authID)
if err != nil {
if err == storage.ErrNotFound {
s.logger.ErrorContext(r.Context(), "invalid 'state' parameter provided", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
s.logger.ErrorContext(r.Context(), "failed to get auth request", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return
}
connID, err := url.PathUnescape(mux.Vars(r)["connector"])
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to parse connector", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
return
} else if connID != "" && connID != authReq.ConnectorID {
s.logger.ErrorContext(r.Context(), "connector mismatch: password login triggered for different connector from authentication start", "start_connector_id", authReq.ConnectorID, "password_connector_id", connID)
s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return
}
conn, err := s.getConnector(ctx, authReq.ConnectorID)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return
}
pwConn, ok := conn.Connector.(connector.PasswordConnector)
if !ok {
s.logger.ErrorContext(r.Context(), "expected password connector in handlePasswordLogin()", "password_connector", pwConn)
s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return
}
switch r.Method {
case http.MethodGet:
if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(pwConn), false, backLink); err != nil {
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
}
case http.MethodPost:
scopes := parseScopes(authReq.Scopes)
identity, ok, err := pwConn.Login(r.Context(), scopes, username, password)
s.logger.ErrorContext(r.Context(), "failed to login user", "err", err)
s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err))
if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(pwConn), true, backLink); err != nil {
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
s.logger.ErrorContext(r.Context(), "failed login attempt: Invalid credentials.", "user", username)
redirectURL, canSkipApproval, err := s.finalizeLogin(r.Context(), identity, authReq, conn.Connector)
s.logger.ErrorContext(r.Context(), "failed to finalize login", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
if canSkipApproval {
authReq, err = s.storage.GetAuthRequest(ctx, authReq.ID)
s.logger.ErrorContext(r.Context(), "failed to get finalized auth request", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return
}
s.sendCodeResponse(w, r, authReq)
return
}
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
s.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
}
}
func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) {
case http.MethodGet: // OAuth2 callback
if authID = r.URL.Query().Get("state"); authID == "" {
s.renderError(r, w, http.StatusBadRequest, "User session error.")
case http.MethodPost: // SAML POST binding
if authID = r.PostFormValue("RelayState"); authID == "" {
s.renderError(r, w, http.StatusBadRequest, "User session error.")
s.renderError(r, w, http.StatusBadRequest, "Method not supported")
return
}
authReq, err := s.storage.GetAuthRequest(ctx, authID)
if err != nil {
if err == storage.ErrNotFound {
s.logger.ErrorContext(r.Context(), "invalid 'state' parameter provided", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
return
}
s.logger.ErrorContext(r.Context(), "failed to get auth request", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return
}
connID, err := url.PathUnescape(mux.Vars(r)["connector"])
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return
} else if connID != "" && connID != authReq.ConnectorID {
s.logger.ErrorContext(r.Context(), "connector mismatch: callback triggered for different connector than authentication start", "authentication_start_connector_id", authReq.ConnectorID, "connector_id", connID)
s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return
}
conn, err := s.getConnector(ctx, authReq.ConnectorID)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
var identity connector.Identity
switch conn := conn.Connector.(type) {
case connector.CallbackConnector:
if r.Method != http.MethodGet {
s.logger.ErrorContext(r.Context(), "SAML request mapped to OAuth2 connector")
s.renderError(r, w, http.StatusBadRequest, "Invalid request")
return
}
identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r)
case connector.SAMLConnector:
if r.Method != http.MethodPost {
s.logger.ErrorContext(r.Context(), "OAuth2 request mapped to SAML connector")
s.renderError(r, w, http.StatusBadRequest, "Invalid request")
identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID)
s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
s.logger.ErrorContext(r.Context(), "failed to authenticate", "err", err)
s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Failed to authenticate: %v", err))
redirectURL, canSkipApproval, err := s.finalizeLogin(ctx, identity, authReq, conn.Connector)
s.logger.ErrorContext(r.Context(), "failed to finalize login", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
if canSkipApproval {
authReq, err = s.storage.GetAuthRequest(ctx, authReq.ID)
s.logger.ErrorContext(r.Context(), "failed to get finalized auth request", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return
}
s.sendCodeResponse(w, r, authReq)
return
}
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
// finalizeLogin associates the user's identity with the current AuthRequest, then returns
// the approval page's path.
func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, bool, error) {
UserID: identity.UserID,
Username: identity.Username,
PreferredUsername: identity.PreferredUsername,
Email: identity.Email,
EmailVerified: identity.EmailVerified,
Groups: identity.Groups,
updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
a.LoggedIn = true
a.Claims = claims
a.ConnectorData = identity.ConnectorData
return a, nil
}
if err := s.storage.UpdateAuthRequest(ctx, authReq.ID, updater); err != nil {
return "", false, fmt.Errorf("failed to update auth request: %v", err)
email := claims.Email
if !claims.EmailVerified {
s.logger.InfoContext(ctx, "login successful",
"connector_id", authReq.ConnectorID, "username", claims.Username,
"preferred_username", claims.PreferredUsername, "email", email, "groups", claims.Groups)
offlineAccessRequested := false
for _, scope := range authReq.Scopes {
if scope == scopeOfflineAccess {
offlineAccessRequested = true
break
}
}
_, canRefresh := conn.(connector.RefreshConnector)
if offlineAccessRequested && canRefresh {
// Try to retrieve an existing OfflineSession object for the corresponding user.
session, err := s.storage.GetOfflineSessions(ctx, identity.UserID, authReq.ConnectorID)
switch {
case err != nil && err == storage.ErrNotFound:
offlineSessions := storage.OfflineSessions{
UserID: identity.UserID,
ConnID: authReq.ConnectorID,
Refresh: make(map[string]*storage.RefreshTokenRef),
ConnectorData: identity.ConnectorData,
}
// Create a new OfflineSession object for the user and add a reference object for
// the newly received refreshtoken.
if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil {
s.logger.ErrorContext(ctx, "failed to create offline session", "err", err)
return "", false, err
}
case err == nil:
// Update existing OfflineSession obj with new RefreshTokenRef.
if err := s.storage.UpdateOfflineSessions(ctx, session.UserID, session.ConnID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
if len(identity.ConnectorData) > 0 {
old.ConnectorData = identity.ConnectorData
}
return old, nil
}); err != nil {
s.logger.ErrorContext(ctx, "failed to update offline session", "err", err)
return "", false, err
}
default:
s.logger.ErrorContext(ctx, "failed to get offline session", "err", err)
return "", false, err
// we can skip the redirect to /approval and go ahead and send code if it's not required
if s.skipApproval && !authReq.ForceApprovalPrompt {
return "", true, nil
// an HMAC is used here to ensure that the request ID is unpredictable, ensuring that an attacker who intercepted the original
// flow would be unable to poll for the result at the /approval endpoint
h := hmac.New(sha256.New, authReq.HMACKey)
h.Write([]byte(authReq.ID))
mac := h.Sum(nil)
returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID + "&hmac=" + base64.RawURLEncoding.EncodeToString(mac)
return returnURL, false, nil
}
func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
macEncoded := r.FormValue("hmac")
if macEncoded == "" {
s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request")
return
}
mac, err := base64.RawURLEncoding.DecodeString(macEncoded)
if err != nil {
s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request")
return
}
authReq, err := s.storage.GetAuthRequest(ctx, r.FormValue("req"))
s.logger.ErrorContext(r.Context(), "failed to get auth request", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Database error.")
if !authReq.LoggedIn {
s.logger.ErrorContext(r.Context(), "auth request does not have an identity for approval")
s.renderError(r, w, http.StatusInternalServerError, "Login process not yet finalized.")
// build expected hmac with secret key
h := hmac.New(sha256.New, authReq.HMACKey)
expectedMAC := h.Sum(nil)
// constant time comparison
if !hmac.Equal(mac, expectedMAC) {
s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request")
return
}
case http.MethodGet:
if r.FormValue("alreadyApproved") == "true" {
s.sendCodeResponse(w, r, authReq)
return
}
client, err := s.storage.GetClient(ctx, authReq.ClientID)
s.logger.ErrorContext(r.Context(), "Failed to get client", "client_id", authReq.ClientID, "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve client.")
approvalSkip := (*approvalSkipData)(nil)
if sub, err := genSubject(authReq.Claims.UserID, authReq.ConnectorID); err == nil {
h := hmac.New(sha256.New, []byte(client.Secret))
h.Write([]byte(client.ID))
h.Write([]byte(sub))
key := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
h = hmac.New(sha256.New, []byte(client.Secret))
h.Write([]byte(client.ID))
scopes := make([]string, len(authReq.Scopes))
copy(scopes, authReq.Scopes)
sort.Strings(scopes)
for _, scope := range scopes {
h.Write([]byte(scope))
}
value := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
approvalSkip = &approvalSkipData{key, value}
}
if err := s.templates.approval(r, w, authReq.ID, authReq.Claims.Username, client.Name, approvalSkip, authReq.Scopes); err != nil {
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
case http.MethodPost:
s.renderError(r, w, http.StatusInternalServerError, "Approval rejected.")
func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authReq storage.AuthRequest) {
s.renderError(r, w, http.StatusBadRequest, "User session has expired.")
if err := s.storage.DeleteAuthRequest(ctx, authReq.ID); err != nil {
s.logger.ErrorContext(r.Context(), "Failed to delete authorization request", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
s.renderError(r, w, http.StatusBadRequest, "User session error.")
}
return
}
u, err := url.Parse(authReq.RedirectURI)
if err != nil {
s.renderError(r, w, http.StatusInternalServerError, "Invalid redirect URI.")
var (
// Was the initial request using the implicit or hybrid flow instead of
// the "normal" code flow?
implicitOrHybrid = false
// Only present in hybrid or code flow. code.ID == "" if this is not set.
code storage.AuthCode
// ID token returned immediately if the response_type includes "id_token".
// Only valid for implicit and hybrid flows.
idToken string
idTokenExpiry time.Time
for _, responseType := range authReq.ResponseTypes {
switch responseType {
case responseTypeCode:
ID: storage.NewID(),
ClientID: authReq.ClientID,
ConnectorID: authReq.ConnectorID,
Nonce: authReq.Nonce,
Scopes: authReq.Scopes,
Claims: authReq.Claims,
Expiry: s.now().Add(time.Minute * 30),
RedirectURI: authReq.RedirectURI,
ConnectorData: authReq.ConnectorData,
if err := s.storage.CreateAuthCode(ctx, code); err != nil {
s.logger.ErrorContext(r.Context(), "Failed to create auth code", "err", err)
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
// Implicit and hybrid flows that try to use the OOB redirect URI are
// rejected earlier. If we got here we're using the code flow.
if err := s.templates.oob(r, w, code.ID); err != nil {
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
implicitOrHybrid = true
case responseTypeIDToken:
implicitOrHybrid = true
var err error
accessToken, _, err = s.newAccessToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID)
s.logger.ErrorContext(r.Context(), "failed to create new access token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return
}
idToken, idTokenExpiry, err = s.newIDToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, code.ID, authReq.ConnectorID)
s.logger.ErrorContext(r.Context(), "failed to create ID token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
}
}
if implicitOrHybrid {
v := url.Values{}
v.Set("token_type", "bearer")
v.Set("state", authReq.State)
if idToken != "" {
// The hybrid flow with only "code token" or "code id_token" doesn't return an
// "expires_in" value. If "code" wasn't provided, indicating the implicit flow,
// don't add it.
//
// https://openid.net/specs/openid-connect-core-1_0.html#HybridAuthResponse
if code.ID == "" {
v.Set("expires_in", strconv.Itoa(int(idTokenExpiry.Sub(s.now()).Seconds())))
}
}
if code.ID != "" {
v.Set("code", code.ID)
// Implicit and hybrid flows return their values as part of the fragment.
//
// HTTP/1.1 303 See Other
// Location: https://client.example.org/cb#
// access_token=SlAV32hkKG
// &token_type=bearer
// &id_token=eyJ0 ... NiJ9.eyJ1c ... I6IjIifX0.DeWt4Qu ... ZXso
// &expires_in=3600
// &state=af0ifjsldkj
//
u.Fragment = v.Encode()
} else {
// The code flow add values to the URL query.
//
// HTTP/1.1 303 See Other
// Location: https://client.example.org/cb?
// code=SplxlOBeZQQYbYS6WxSbIA
// &state=af0ifjsldkj
//
q := u.Query()
q.Set("code", code.ID)
q.Set("state", authReq.State)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusSeeOther)
}
func (s *Server) withClientFromStorage(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request, storage.Client)) {
clientID, clientSecret, ok := r.BasicAuth()
if ok {
var err error
if clientID, err = url.QueryUnescape(clientID); err != nil {
s.tokenErrHelper(w, errInvalidRequest, "client_id improperly encoded", http.StatusBadRequest)
return
}
if clientSecret, err = url.QueryUnescape(clientSecret); err != nil {
s.tokenErrHelper(w, errInvalidRequest, "client_secret improperly encoded", http.StatusBadRequest)
return
}
} else {
clientID = r.PostFormValue("client_id")
clientSecret = r.PostFormValue("client_secret")
}
client, err := s.storage.GetClient(ctx, clientID)
s.logger.ErrorContext(r.Context(), "failed to get client", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
if subtle.ConstantTimeCompare([]byte(client.Secret), []byte(clientSecret)) != 1 {
s.logger.InfoContext(r.Context(), "missing client_secret on token request", "client_id", client.ID)
s.logger.InfoContext(r.Context(), "invalid client_secret on token request", "client_id", client.ID)
s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
handler(w, r, client)
}
func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
s.tokenErrHelper(w, errInvalidRequest, "method not allowed", http.StatusBadRequest)
return
}
err := r.ParseForm()
if err != nil {
s.logger.ErrorContext(r.Context(), "could not parse request body", "err", err)
s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest)
return
}
if !contains(s.supportedGrantTypes, grantType) {
s.logger.ErrorContext(r.Context(), "unsupported grant type", "grant_type", grantType)
s.tokenErrHelper(w, errUnsupportedGrantType, "", http.StatusBadRequest)
return
}
case grantTypeDeviceCode:
s.handleDeviceToken(w, r)
case grantTypeAuthorizationCode:
s.withClientFromStorage(w, r, s.handleAuthCode)
case grantTypeRefreshToken:
s.withClientFromStorage(w, r, s.handleRefreshToken)
s.withClientFromStorage(w, r, s.handlePasswordGrant)
case grantTypeTokenExchange:
s.withClientFromStorage(w, r, s.handleTokenExchange)
s.tokenErrHelper(w, errUnsupportedGrantType, "", http.StatusBadRequest)
func (s *Server) calculateCodeChallenge(codeVerifier, codeChallengeMethod string) (string, error) {
switch codeChallengeMethod {
shaSum := sha256.Sum256([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(shaSum[:]), nil
default:
return "", fmt.Errorf("unknown challenge method (%v)", codeChallengeMethod)
}
}
// handle an access token request https://tools.ietf.org/html/rfc6749#section-4.1.3
func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client storage.Client) {
code := r.PostFormValue("code")
redirectURI := r.PostFormValue("redirect_uri")
if code == "" {
s.tokenErrHelper(w, errInvalidRequest, `Required param: code.`, http.StatusBadRequest)
return
}
authCode, err := s.storage.GetAuthCode(ctx, code)
if err != nil || s.now().After(authCode.Expiry) || authCode.ClientID != client.ID {
if err != storage.ErrNotFound {
s.logger.ErrorContext(r.Context(), "failed to get auth code", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
s.tokenErrHelper(w, errInvalidGrant, "Invalid or expired code parameter.", http.StatusBadRequest)
// RFC 7636 (PKCE)
codeChallengeFromStorage := authCode.PKCE.CodeChallenge
providedCodeVerifier := r.PostFormValue("code_verifier")
switch {
case providedCodeVerifier != "" && codeChallengeFromStorage != "":
calculatedCodeChallenge, err := s.calculateCodeChallenge(providedCodeVerifier, authCode.PKCE.CodeChallengeMethod)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to calculate code challenge", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return
}
if codeChallengeFromStorage != calculatedCodeChallenge {
s.tokenErrHelper(w, errInvalidGrant, "Invalid code_verifier.", http.StatusBadRequest)
return
}
case providedCodeVerifier != "":
// Received no code_challenge on /auth, but a code_verifier on /token
s.tokenErrHelper(w, errInvalidRequest, "No PKCE flow started. Cannot check code_verifier.", http.StatusBadRequest)
return
case codeChallengeFromStorage != "":
// Received PKCE request on /auth, but no code_verifier on /token
s.tokenErrHelper(w, errInvalidGrant, "Expecting parameter code_verifier in PKCE flow.", http.StatusBadRequest)
return
}
s.tokenErrHelper(w, errInvalidRequest, "redirect_uri did not match URI from initial request.", http.StatusBadRequest)
tokenResponse, err := s.exchangeAuthCode(ctx, w, authCode, client)
if err != nil {
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return
}
s.writeAccessToken(w, tokenResponse)
}
func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, authCode storage.AuthCode, client storage.Client) (*accessTokenResponse, error) {
accessToken, _, err := s.newAccessToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID)
s.logger.ErrorContext(ctx, "failed to create new access token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
idToken, expiry, err := s.newIDToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ID, authCode.ConnectorID)
s.logger.ErrorContext(ctx, "failed to create ID token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
if err := s.storage.DeleteAuthCode(ctx, authCode.ID); err != nil {
s.logger.ErrorContext(ctx, "failed to delete auth code", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
rithu john
committed
// Ensure the connector supports refresh tokens.
//
// Connectors like `saml` do not implement RefreshConnector.
conn, err := s.getConnector(ctx, authCode.ConnectorID)
if err != nil {
s.logger.ErrorContext(ctx, "connector not found", "connector_id", authCode.ConnectorID, "err", err)
rithu john
committed
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return false
}
_, ok := conn.Connector.(connector.RefreshConnector)