Skip to content
Snippets Groups Projects
handlers.go 33.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • Eric Chiang's avatar
    Eric Chiang committed
    package server
    
    import (
    	"encoding/json"
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"fmt"
    	"net/http"
    	"net/url"
    	"path"
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"strconv"
    	"strings"
    	"time"
    
    	"github.com/gorilla/mux"
    	jose "gopkg.in/square/go-jose.v2"
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"github.com/coreos/dex/connector"
    
    	"github.com/coreos/dex/server/internal"
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"github.com/coreos/dex/storage"
    
    Eric Chiang's avatar
    Eric Chiang committed
    )
    
    
    func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
    	start := s.now()
    	err := func() error {
    		// Instead of trying to introspect health, just try to use the underlying storage.
    		a := storage.AuthRequest{
    			ID:       storage.NewID(),
    			ClientID: storage.NewID(),
    
    			// Set a short expiry so if the delete fails this will be cleaned up quickly by garbage collection.
    			Expiry: s.now().Add(time.Minute),
    		}
    
    		if err := s.storage.CreateAuthRequest(a); err != nil {
    			return fmt.Errorf("create auth request: %v", err)
    		}
    		if err := s.storage.DeleteAuthRequest(a.ID); err != nil {
    			return fmt.Errorf("delete auth request: %v", err)
    		}
    		return nil
    	}()
    
    	t := s.now().Sub(start)
    	if err != nil {
    
    		s.logger.Errorf("Storage health check failed: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Health check failed.")
    
    		return
    	}
    	fmt.Fprintf(w, "Health check passed in %s", t)
    }
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
    	// TODO(ericchiang): Cache this.
    	keys, err := s.storage.GetKeys()
    	if err != nil {
    
    		s.logger.Errorf("failed to get keys: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Internal server error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	if keys.SigningKeyPub == nil {
    
    		s.logger.Errorf("No public keys found.")
    
    		s.renderError(w, http.StatusInternalServerError, "Internal server error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		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.Errorf("failed to marshal discovery data: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Internal server error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		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())))
    
    Eric Chiang's avatar
    Eric Chiang committed
    	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"`
    	ResponseTypes []string `json:"response_types_supported"`
    	Subjects      []string `json:"subject_types_supported"`
    	IDTokenAlgs   []string `json:"id_token_signing_alg_values_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) {
    
    Eric Chiang's avatar
    Eric Chiang committed
    	d := discovery{
    
    		Issuer:      s.issuerURL.String(),
    		Auth:        s.absURL("/auth"),
    		Token:       s.absURL("/token"),
    		Keys:        s.absURL("/keys"),
    		Subjects:    []string{"public"},
    		IDTokenAlgs: []string{string(jose.RS256)},
    
    Phu Kieu's avatar
    Phu Kieu committed
    		Scopes:      []string{"openid", "email", "groups", "profile", "offline_access"},
    
    		AuthMethods: []string{"client_secret_basic"},
    
    Eric Chiang's avatar
    Eric Chiang committed
    		Claims: []string{
    
    			"aud", "email", "email_verified", "exp",
    
    Eric Chiang's avatar
    Eric Chiang committed
    			"iat", "iss", "locale", "name", "sub",
    		},
    	}
    
    
    	for responseType := range s.supportedResponseTypes {
    		d.ResponseTypes = append(d.ResponseTypes, responseType)
    	}
    	sort.Strings(d.ResponseTypes)
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    	data, err := json.MarshalIndent(d, "", "  ")
    	if err != nil {
    
    		return nil, fmt.Errorf("failed to marshal discovery data: %v", err)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    
    	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)
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    // handleAuthorization handles the OAuth2 auth endpoint.
    func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
    
    	authReq, err := s.parseAuthorizationRequest(r)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err != nil {
    
    		s.logger.Errorf("Failed to parse authorization request: %v", err)
    
    		if handler, ok := err.Handle(); ok {
    			// client_id and redirect_uri checked out and we can redirect back to
    			// the client with the error.
    			handler.ServeHTTP(w, r)
    			return
    		}
    
    		// Otherwise render the error to the user.
    		//
    		// TODO(ericchiang): Should we just always render the error?
    		s.renderError(w, err.Status(), err.Error())
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	// TODO(ericchiang): Create this authorization request later in the login flow
    	// so users don't hit "not found" database errors if they wait at the login
    	// screen too long.
    	//
    	// See: https://github.com/coreos/dex/issues/646
    	authReq.Expiry = s.now().Add(24 * time.Hour) // Totally arbitrary value.
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err := s.storage.CreateAuthRequest(authReq); err != nil {
    
    		s.logger.Errorf("Failed to create authorization request: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Failed to connect to the database.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	connectors, e := s.storage.ListConnectors()
    	if e != nil {
    		s.logger.Errorf("Failed to get list of connectors: %v", err)
    		s.renderError(w, http.StatusInternalServerError, "Failed to retrieve connector list.")
    		return
    	}
    
    	if len(connectors) == 1 {
    		for _, c := range connectors {
    
    			// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
    			// on create the auth request.
    
    			http.Redirect(w, r, s.absPath("/auth", c.ID)+"?req="+authReq.ID, http.StatusFound)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    	}
    
    
    	connectorInfos := make([]connectorInfo, len(connectors))
    
    Eric Chiang's avatar
    Eric Chiang committed
    	i := 0
    
    	for _, conn := range connectors {
    
    Eric Chiang's avatar
    Eric Chiang committed
    		connectorInfos[i] = connectorInfo{
    
    			// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
    			// on create the auth request.
    
    			URL: s.absPath("/auth", conn.ID) + "?req=" + authReq.ID,
    
    	if err := s.templates.login(w, connectorInfos); err != nil {
    
    		s.logger.Errorf("Server template error: %v", err)
    	}
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
    	connID := mux.Vars(r)["connector"]
    
    	conn, err := s.getConnector(connID)
    	if err != nil {
    		s.logger.Errorf("Failed to create authorization request: %v", err)
    		s.renderError(w, http.StatusBadRequest, "Requested resource does not exist")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    
    	authReqID := r.FormValue("req")
    
    	authReq, err := s.storage.GetAuthRequest(authReqID)
    	if err != nil {
    
    		s.logger.Errorf("Failed to get auth request: %v", err)
    
    		if err == storage.ErrNotFound {
    			s.renderError(w, http.StatusBadRequest, "Login session expired.")
    		} else {
    			s.renderError(w, http.StatusInternalServerError, "Database error.")
    		}
    
    		return
    	}
    	scopes := parseScopes(authReq.Scopes)
    
    	showBacklink := len(s.connectors) > 1
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    	switch r.Method {
    	case "GET":
    
    		// Set the connector being used for the login.
    		updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
    			a.ConnectorID = connID
    			return a, nil
    		}
    		if err := s.storage.UpdateAuthRequest(authReqID, updater); err != nil {
    
    			s.logger.Errorf("Failed to set connector ID on auth request: %v", err)
    
    			s.renderError(w, http.StatusInternalServerError, "Database error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		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"), authReqID)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			if err != nil {
    
    				s.logger.Errorf("Connector %q returned error when creating callback: %v", connID, err)
    
    				s.renderError(w, http.StatusInternalServerError, "Login error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    				return
    			}
    			http.Redirect(w, r, callbackURL, http.StatusFound)
    		case connector.PasswordConnector:
    
    			if err := s.templates.password(w, r.URL.String(), "", usernamePrompt(conn), false, showBacklink); err != nil {
    
    				s.logger.Errorf("Server template error: %v", err)
    			}
    
    		case connector.SAMLConnector:
    
    			action, value, err := conn.POSTData(scopes, authReqID)
    
    			if err != nil {
    				s.logger.Errorf("Creating SAML data: %v", err)
    				s.renderError(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, authReqID)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		default:
    
    			s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		}
    	case "POST":
    		passwordConnector, ok := conn.Connector.(connector.PasswordConnector)
    		if !ok {
    
    			s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    
    
    		username := r.FormValue("login")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		password := r.FormValue("password")
    
    
    		identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		if err != nil {
    
    			s.logger.Errorf("Failed to login user: %v", err)
    
    			s.renderError(w, http.StatusInternalServerError, "Login error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    		if !ok {
    
    			if err := s.templates.password(w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink); err != nil {
    
    				s.logger.Errorf("Server template error: %v", err)
    			}
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    
    		redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		if err != nil {
    
    			s.logger.Errorf("Failed to finalize login: %v", err)
    
    			s.renderError(w, http.StatusInternalServerError, "Login error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    
    
    		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	default:
    
    		s.renderError(w, http.StatusBadRequest, "Unsupported request method.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    }
    
    func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) {
    
    	var authID string
    	switch r.Method {
    	case "GET": // OAuth2 callback
    		if authID = r.URL.Query().Get("state"); authID == "" {
    			s.renderError(w, http.StatusBadRequest, "User session error.")
    			return
    		}
    	case "POST": // SAML POST binding
    		if authID = r.PostFormValue("RelayState"); authID == "" {
    			s.renderError(w, http.StatusBadRequest, "User session error.")
    			return
    		}
    	default:
    		s.renderError(w, http.StatusBadRequest, "Method not supported")
    
    	authReq, err := s.storage.GetAuthRequest(authID)
    
    	if err != nil {
    		if err == storage.ErrNotFound {
    
    			s.logger.Errorf("Invalid 'state' parameter provided: %v", err)
    			s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
    
    		s.logger.Errorf("Failed to get auth request: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Database error.")
    
    	if connID := mux.Vars(r)["connector"]; connID != "" && connID != authReq.ConnectorID {
    		s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID)
    		s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
    		return
    	}
    
    
    	conn, err := s.getConnector(authReq.ConnectorID)
    	if err != nil {
    		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
    
    		s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    
    	var identity connector.Identity
    	switch conn := conn.Connector.(type) {
    	case connector.CallbackConnector:
    		if r.Method != "GET" {
    			s.logger.Errorf("SAML request mapped to OAuth2 connector")
    			s.renderError(w, http.StatusBadRequest, "Invalid request")
    			return
    		}
    		identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r)
    	case connector.SAMLConnector:
    		if r.Method != "POST" {
    			s.logger.Errorf("OAuth2 request mapped to SAML connector")
    			s.renderError(w, http.StatusBadRequest, "Invalid request")
    			return
    		}
    
    		identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID)
    
    	default:
    
    		s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	if err != nil {
    
    		s.logger.Errorf("Failed to authenticate: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Failed to return user's identity.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err != nil {
    
    		s.logger.Errorf("Failed to finalize login: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Login error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    
    	http.Redirect(w, r, redirectURL, http.StatusSeeOther)
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    
    // finalizeLogin associates the user's identity with the current AuthRequest, then returns
    // the approval page's path.
    
    func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, error) {
    
    Eric Chiang's avatar
    Eric Chiang committed
    	claims := storage.Claims{
    
    		UserID:        identity.UserID,
    		Username:      identity.Username,
    		Email:         identity.Email,
    		EmailVerified: identity.EmailVerified,
    
    		Groups:        identity.Groups,
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    
    
    	updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
    
    		a.ConnectorData = identity.ConnectorData
    		return a, nil
    	}
    
    	if err := s.storage.UpdateAuthRequest(authReq.ID, updater); err != nil {
    
    		return "", fmt.Errorf("failed to update auth request: %v", err)
    	}
    
    
    	email := claims.Email
    	if !claims.EmailVerified {
    		email = email + " (unverified)"
    	}
    
    	s.logger.Infof("login successful: connector %q, username=%q, email=%q, groups=%q",
    		authReq.ConnectorID, claims.Username, email, claims.Groups)
    
    
    	return path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID, nil
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
    
    	authReq, err := s.storage.GetAuthRequest(r.FormValue("req"))
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err != nil {
    
    		s.logger.Errorf("Failed to get auth request: %v", err)
    
    		s.renderError(w, http.StatusInternalServerError, "Database error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    		s.logger.Errorf("Auth request does not have an identity for approval")
    
    		s.renderError(w, http.StatusInternalServerError, "Login process not yet finalized.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	switch r.Method {
    	case "GET":
    		if s.skipApproval {
    
    Eric Chiang's avatar
    Eric Chiang committed
    			s.sendCodeResponse(w, r, authReq)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    		client, err := s.storage.GetClient(authReq.ClientID)
    		if err != nil {
    
    			s.logger.Errorf("Failed to get client %q: %v", authReq.ClientID, err)
    
    			s.renderError(w, http.StatusInternalServerError, "Failed to retrieve client.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    
    		if err := s.templates.approval(w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes); err != nil {
    			s.logger.Errorf("Server template error: %v", err)
    		}
    
    Eric Chiang's avatar
    Eric Chiang committed
    	case "POST":
    		if r.FormValue("approval") != "approve" {
    
    			s.renderError(w, http.StatusInternalServerError, "Approval rejected.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    
    Eric Chiang's avatar
    Eric Chiang committed
    		s.sendCodeResponse(w, r, authReq)
    
    Eric Chiang's avatar
    Eric Chiang committed
    func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authReq storage.AuthRequest) {
    
    	if s.now().After(authReq.Expiry) {
    
    		s.renderError(w, http.StatusBadRequest, "User session has expired.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	if err := s.storage.DeleteAuthRequest(authReq.ID); err != nil {
    		if err != storage.ErrNotFound {
    
    			s.logger.Errorf("Failed to delete authorization request: %v", err)
    
    			s.renderError(w, http.StatusInternalServerError, "Internal server error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		} else {
    
    			s.renderError(w, http.StatusBadRequest, "User session error.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		}
    		return
    	}
    	u, err := url.Parse(authReq.RedirectURI)
    	if err != nil {
    
    		s.renderError(w, http.StatusInternalServerError, "Invalid redirect URI.")
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    
    	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
    
    
    		accessToken = storage.NewID()
    
    
    	for _, responseType := range authReq.ResponseTypes {
    		switch responseType {
    		case responseTypeCode:
    
    			code = storage.AuthCode{
    
    				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(code); err != nil {
    
    				s.logger.Errorf("Failed to create auth code: %v", err)
    
    				s.renderError(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 authReq.RedirectURI == redirectURIOOB {
    
    				if err := s.templates.oob(w, code.ID); err != nil {
    					s.logger.Errorf("Server template error: %v", err)
    				}
    
    				return
    			}
    		case responseTypeToken:
    
    			implicitOrHybrid = true
    		case responseTypeIDToken:
    			implicitOrHybrid = true
    			var err error
    
    			idToken, idTokenExpiry, err = s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, authReq.ConnectorID)
    
    			if err != nil {
    
    				s.logger.Errorf("failed to create ID token: %v", err)
    				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    		}
    	}
    
    	if implicitOrHybrid {
    		v := url.Values{}
    
    		v.Set("access_token", accessToken)
    
    		v.Set("token_type", "bearer")
    		v.Set("state", authReq.State)
    		if idToken != "" {
    
    			v.Set("id_token", 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()
    
    Eric Chiang's avatar
    Eric Chiang committed
    	http.Redirect(w, r, u.String(), http.StatusSeeOther)
    }
    
    func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
    	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)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    		if clientSecret, err = url.QueryUnescape(clientSecret); err != nil {
    
    			s.tokenErrHelper(w, errInvalidRequest, "client_secret improperly encoded", http.StatusBadRequest)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    	} else {
    		clientID = r.PostFormValue("client_id")
    		clientSecret = r.PostFormValue("client_secret")
    	}
    
    	client, err := s.storage.GetClient(clientID)
    	if err != nil {
    		if err != storage.ErrNotFound {
    
    			s.logger.Errorf("failed to get client: %v", err)
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		} else {
    
    			s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		}
    		return
    	}
    	if client.Secret != clientSecret {
    
    		s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	grantType := r.PostFormValue("grant_type")
    	switch grantType {
    
    	case grantTypeAuthorizationCode:
    
    Eric Chiang's avatar
    Eric Chiang committed
    		s.handleAuthCode(w, r, client)
    
    	case grantTypeRefreshToken:
    
    Eric Chiang's avatar
    Eric Chiang committed
    		s.handleRefreshToken(w, r, client)
    	default:
    
    		s.tokenErrHelper(w, errInvalidGrant, "", http.StatusBadRequest)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    }
    
    // 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")
    
    	authCode, err := s.storage.GetAuthCode(code)
    	if err != nil || s.now().After(authCode.Expiry) || authCode.ClientID != client.ID {
    		if err != storage.ErrNotFound {
    
    			s.logger.Errorf("failed to get auth code: %v", err)
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		} else {
    
    			s.tokenErrHelper(w, errInvalidRequest, "Invalid or expired code parameter.", http.StatusBadRequest)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		}
    		return
    	}
    
    	if authCode.RedirectURI != redirectURI {
    
    		s.tokenErrHelper(w, errInvalidRequest, "redirect_uri did not match URI from initial request.", http.StatusBadRequest)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    
    	accessToken := storage.NewID()
    
    	idToken, expiry, err := s.newIDToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ConnectorID)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err != nil {
    
    		s.logger.Errorf("failed to create ID token: %v", err)
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	if err := s.storage.DeleteAuthCode(code); err != nil {
    
    		s.logger.Errorf("failed to delete auth code: %v", err)
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	reqRefresh := func() bool {
    
    		// Ensure the connector supports refresh tokens.
    		//
    
    Eric Chiang's avatar
    Eric Chiang committed
    		// Connectors like `saml` do not implement RefreshConnector.
    
    		conn, err := s.getConnector(authCode.ConnectorID)
    		if err != nil {
    			s.logger.Errorf("connector with ID %q not found: %v", authCode.ConnectorID, err)
    
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    			return false
    		}
    
    
    		_, ok := conn.Connector.(connector.RefreshConnector)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		for _, scope := range authCode.Scopes {
    			if scope == scopeOfflineAccess {
    				return true
    			}
    		}
    		return false
    	}()
    	var refreshToken string
    	if reqRefresh {
    
    Eric Chiang's avatar
    Eric Chiang committed
    		refresh := storage.RefreshToken{
    
    			ID:            storage.NewID(),
    			Token:         storage.NewID(),
    
    			ClientID:      authCode.ClientID,
    			ConnectorID:   authCode.ConnectorID,
    			Scopes:        authCode.Scopes,
    			Claims:        authCode.Claims,
    			Nonce:         authCode.Nonce,
    			ConnectorData: authCode.ConnectorData,
    
    			CreatedAt:     s.now(),
    			LastUsed:      s.now(),
    
    Eric Chiang's avatar
    Eric Chiang committed
    		}
    
    		token := &internal.RefreshToken{
    			RefreshId: refresh.ID,
    			Token:     refresh.Token,
    		}
    		if refreshToken, err = internal.Marshal(token); err != nil {
    			s.logger.Errorf("failed to marshal refresh token: %v", err)
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    			return
    		}
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    		if err := s.storage.CreateRefresh(refresh); err != nil {
    
    			s.logger.Errorf("failed to create refresh token: %v", err)
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    
    
    		// deleteToken determines if we need to delete the newly created refresh token
    		// due to a failure in updating/creating the OfflineSession object for the
    		// corresponding user.
    		var deleteToken bool
    		defer func() {
    			if deleteToken {
    				// Delete newly created refresh token from storage.
    				if err := s.storage.DeleteRefresh(refresh.ID); err != nil {
    					s.logger.Errorf("failed to delete refresh token: %v", err)
    					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    					return
    				}
    			}
    		}()
    
    		tokenRef := storage.RefreshTokenRef{
    			ID:        refresh.ID,
    			ClientID:  refresh.ClientID,
    			CreatedAt: refresh.CreatedAt,
    			LastUsed:  refresh.LastUsed,
    		}
    
    		// Try to retrieve an existing OfflineSession object for the corresponding user.
    		if session, err := s.storage.GetOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID); err != nil {
    			if err != storage.ErrNotFound {
    				s.logger.Errorf("failed to get offline session: %v", err)
    				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    				deleteToken = true
    				return
    			}
    			offlineSessions := storage.OfflineSessions{
    				UserID:  refresh.Claims.UserID,
    				ConnID:  refresh.ConnectorID,
    				Refresh: make(map[string]*storage.RefreshTokenRef),
    			}
    			offlineSessions.Refresh[tokenRef.ClientID] = &tokenRef
    
    			// Create a new OfflineSession object for the user and add a reference object for
    
    			// the newly received refreshtoken.
    
    			if err := s.storage.CreateOfflineSessions(offlineSessions); err != nil {
    				s.logger.Errorf("failed to create offline session: %v", err)
    				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    				deleteToken = true
    				return
    			}
    		} else {
    			if oldTokenRef, ok := session.Refresh[tokenRef.ClientID]; ok {
    				// Delete old refresh token from storage.
    				if err := s.storage.DeleteRefresh(oldTokenRef.ID); err != nil {
    					s.logger.Errorf("failed to delete refresh token: %v", err)
    					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    					deleteToken = true
    					return
    				}
    			}
    
    			// Update existing OfflineSession obj with new RefreshTokenRef.
    			if err := s.storage.UpdateOfflineSessions(session.UserID, session.ConnID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
    				old.Refresh[tokenRef.ClientID] = &tokenRef
    				return old, nil
    			}); err != nil {
    				s.logger.Errorf("failed to update offline session: %v", err)
    				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    				deleteToken = true
    				return
    			}
    
    		}
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    
    	s.writeAccessToken(w, idToken, accessToken, refreshToken, expiry)
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    // handle a refresh token request https://tools.ietf.org/html/rfc6749#section-6
    func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, client storage.Client) {
    	code := r.PostFormValue("refresh_token")
    	scope := r.PostFormValue("scope")
    	if code == "" {
    
    		s.tokenErrHelper(w, errInvalidRequest, "No refresh token in request.", http.StatusBadRequest)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    
    	token := new(internal.RefreshToken)
    	if err := internal.Unmarshal(code, token); err != nil {
    		// For backward compatibility, assume the refresh_token is a raw refresh token ID
    		// if it fails to decode.
    		//
    		// Because refresh_token values that aren't unmarshable were generated by servers
    		// that don't have a Token value, we'll still reject any attempts to claim a
    		// refresh_token twice.
    		token = &internal.RefreshToken{RefreshId: code, Token: ""}
    	}
    
    	refresh, err := s.storage.GetRefresh(token.RefreshId)
    	if err != nil {
    		s.logger.Errorf("failed to get refresh token: %v", err)
    		if err == storage.ErrNotFound {
    
    			s.tokenErrHelper(w, errInvalidRequest, "Refresh token is invalid or has already been claimed by another client.", http.StatusBadRequest)
    
    		} else {
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		}
    		return
    	}
    
    	if refresh.ClientID != client.ID {
    		s.logger.Errorf("client %s trying to claim token for client %s", client.ID, refresh.ClientID)
    		s.tokenErrHelper(w, errInvalidRequest, "Refresh token is invalid or has already been claimed by another client.", http.StatusBadRequest)
    		return
    	}
    	if refresh.Token != token.Token {
    		s.logger.Errorf("refresh token with id %s claimed twice", refresh.ID)
    		s.tokenErrHelper(w, errInvalidRequest, "Refresh token is invalid or has already been claimed by another client.", http.StatusBadRequest)
    		return
    	}
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    	// Per the OAuth2 spec, if the client has omitted the scopes, default to the original
    	// authorized scopes.
    	//
    	// https://tools.ietf.org/html/rfc6749#section-6
    
    Eric Chiang's avatar
    Eric Chiang committed
    	scopes := refresh.Scopes
    	if scope != "" {
    
    		requestedScopes := strings.Fields(scope)
    
    		var unauthorizedScopes []string
    
    		for _, s := range requestedScopes {
    			contains := func() bool {
    
    Eric Chiang's avatar
    Eric Chiang committed
    				for _, scope := range refresh.Scopes {
    					if s == scope {
    
    Eric Chiang's avatar
    Eric Chiang committed
    					}
    				}
    				return false
    
    			}()
    			if !contains {
    				unauthorizedScopes = append(unauthorizedScopes, s)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			}
    
    		}
    
    		if len(unauthorizedScopes) > 0 {
    			msg := fmt.Sprintf("Requested scopes contain unauthorized scope(s): %q.", unauthorizedScopes)
    
    			s.tokenErrHelper(w, errInvalidRequest, msg, http.StatusBadRequest)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			return
    		}
    		scopes = requestedScopes
    	}
    
    
    	conn, err := s.getConnector(refresh.ConnectorID)
    	if err != nil {
    		s.logger.Errorf("connector with ID %q not found: %v", refresh.ConnectorID, err)
    
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    	ident := connector.Identity{
    		UserID:        refresh.Claims.UserID,
    		Username:      refresh.Claims.Username,
    		Email:         refresh.Claims.Email,
    		EmailVerified: refresh.Claims.EmailVerified,
    		Groups:        refresh.Claims.Groups,
    		ConnectorData: refresh.ConnectorData,
    	}
    
    
    	// Can the connector refresh the identity? If so, attempt to refresh the data
    	// in the connector.
    	//
    	// TODO(ericchiang): We may want a strict mode where connectors that don't implement
    	// this interface can't perform refreshing.
    	if refreshConn, ok := conn.Connector.(connector.RefreshConnector); ok {
    
    		newIdent, err := refreshConn.Refresh(r.Context(), parseScopes(scopes), ident)
    
    		if err != nil {
    
    			s.logger.Errorf("failed to refresh identity: %v", err)
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    	claims := storage.Claims{
    		UserID:        ident.UserID,
    		Username:      ident.Username,
    		Email:         ident.Email,
    		EmailVerified: ident.EmailVerified,
    		Groups:        ident.Groups,
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    	accessToken := storage.NewID()
    
    	idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, refresh.Nonce, accessToken, refresh.ConnectorID)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err != nil {
    
    		s.logger.Errorf("failed to create ID token: %v", err)
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    
    	newToken := &internal.RefreshToken{
    		RefreshId: refresh.ID,
    		Token:     storage.NewID(),
    	}
    	rawNewToken, err := internal.Marshal(newToken)
    	if err != nil {
    		s.logger.Errorf("failed to marshal refresh token: %v", err)
    
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	updater := func(old storage.RefreshToken) (storage.RefreshToken, error) {
    		if old.Token != refresh.Token {
    			return old, errors.New("refresh token claimed twice")
    		}
    		old.Token = newToken.Token
    		// Update the claims of the refresh token.
    		//
    		// UserID intentionally ignored for now.
    		old.Claims.Username = ident.Username
    		old.Claims.Email = ident.Email
    		old.Claims.EmailVerified = ident.EmailVerified
    		old.Claims.Groups = ident.Groups
    		old.ConnectorData = ident.ConnectorData
    
    		old.LastUsed = lastUsed
    
    
    	// Update LastUsed time stamp in refresh token reference object
    	// in offline session for the user.
    	if err := s.storage.UpdateOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
    		if old.Refresh[refresh.ClientID].ID != refresh.ID {
    			return old, errors.New("refresh token invalid")
    		}
    		old.Refresh[refresh.ClientID].LastUsed = lastUsed
    		return old, nil
    	}); err != nil {
    		s.logger.Errorf("failed to update offline session: %v", err)
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    		return
    	}
    
    	// Update refresh token in the storage.
    
    	if err := s.storage.UpdateRefreshToken(refresh.ID, updater); err != nil {
    		s.logger.Errorf("failed to update refresh token: %v", err)
    
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    
    	s.writeAccessToken(w, idToken, accessToken, rawNewToken, expiry)
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    
    func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, accessToken, refreshToken string, expiry time.Time) {
    
    Eric Chiang's avatar
    Eric Chiang committed
    	// TODO(ericchiang): figure out an access token story and support the user info
    	// endpoint. For now use a random value so no one depends on the access_token
    	// holding a specific structure.
    	resp := struct {
    		AccessToken  string `json:"access_token"`
    		TokenType    string `json:"token_type"`
    		ExpiresIn    int    `json:"expires_in"`
    		RefreshToken string `json:"refresh_token,omitempty"`
    		IDToken      string `json:"id_token"`
    	}{
    
    		accessToken,
    
    Eric Chiang's avatar
    Eric Chiang committed
    		"bearer",
    
    		int(expiry.Sub(s.now()).Seconds()),
    
    Eric Chiang's avatar
    Eric Chiang committed
    		refreshToken,
    		idToken,
    	}
    	data, err := json.Marshal(resp)
    	if err != nil {
    
    		s.logger.Errorf("failed to marshal access token response: %v", err)
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		return
    	}
    	w.Header().Set("Content-Type", "application/json")
    	w.Header().Set("Content-Length", strconv.Itoa(len(data)))
    	w.Write(data)
    }
    
    
    func (s *Server) renderError(w http.ResponseWriter, status int, description string) {
    
    	if err := s.templates.err(w, status, description); err != nil {
    
    		s.logger.Errorf("Server template error: %v", err)