Skip to content
Snippets Groups Projects
handlers.go 47.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • Eric Chiang's avatar
    Eric Chiang committed
    		scopes = requestedScopes
    	}
    
    
    	var connectorData []byte
    
    m.nabokikh's avatar
    m.nabokikh committed
    
    	session, err := s.storage.GetOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID)
    	switch {
    	case err != nil:
    
    		if err != storage.ErrNotFound {
    			s.logger.Errorf("failed to get offline session: %v", err)
    			return
    		}
    
    m.nabokikh's avatar
    m.nabokikh committed
    	case len(refresh.ConnectorData) > 0:
    
    		// Use the old connector data if it exists, should be deleted once used
    
    		connectorData = refresh.ConnectorData
    
    m.nabokikh's avatar
    m.nabokikh committed
    	default:
    
    		connectorData = session.ConnectorData
    	}
    
    
    	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)
    
    		UserID:            refresh.Claims.UserID,
    		Username:          refresh.Claims.Username,
    		PreferredUsername: refresh.Claims.PreferredUsername,
    		Email:             refresh.Claims.Email,
    		EmailVerified:     refresh.Claims.EmailVerified,
    		Groups:            refresh.Claims.Groups,
    
    		ConnectorData:     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)
    
    		UserID:            ident.UserID,
    		Username:          ident.Username,
    		PreferredUsername: ident.PreferredUsername,
    		Email:             ident.Email,
    		EmailVerified:     ident.EmailVerified,
    		Groups:            ident.Groups,
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    	accessToken, err := s.newAccessToken(client.ID, claims, scopes, refresh.Nonce, refresh.ConnectorID)
    	if err != nil {
    		s.logger.Errorf("failed to create new access token: %v", err)
    		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    		return
    	}
    
    
    	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.PreferredUsername = ident.PreferredUsername
    
    		old.Claims.Email = ident.Email
    		old.Claims.EmailVerified = ident.EmailVerified
    		old.Claims.Groups = ident.Groups
    
    		old.LastUsed = lastUsed
    
    
    		// ConnectorData has been moved to OfflineSession
    		old.ConnectorData = []byte{}
    
    
    	// 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
    
    		old.ConnectorData = ident.ConnectorData
    
    		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
    	}
    
    	resp := s.toAccessTokenResponse(idToken, accessToken, rawNewToken, expiry)
    	s.writeAccessToken(w, resp)
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    
    func (s *Server) handleUserInfo(w http.ResponseWriter, r *http.Request) {
    
    	const prefix = "Bearer "
    
    	auth := r.Header.Get("authorization")
    	if len(auth) < len(prefix) || !strings.EqualFold(prefix, auth[:len(prefix)]) {
    		w.Header().Set("WWW-Authenticate", "Bearer")
    		s.tokenErrHelper(w, errAccessDenied, "Invalid bearer token.", http.StatusUnauthorized)
    
    	rawIDToken := auth[len(prefix):]
    
    	verifier := oidc.NewVerifier(s.issuerURL.String(), &storageKeySet{s.storage}, &oidc.Config{SkipClientIDCheck: true})
    	idToken, err := verifier.Verify(r.Context(), rawIDToken)
    
    	if err != nil {
    
    		s.tokenErrHelper(w, errAccessDenied, err.Error(), http.StatusForbidden)
    
    	var claims json.RawMessage
    	if err := idToken.Claims(&claims); err != nil {
    		s.tokenErrHelper(w, errServerError, err.Error(), http.StatusInternalServerError)
    		return
    
    	w.Header().Set("Content-Type", "application/json")
    	w.Write(claims)
    
    func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, client storage.Client) {
    	// Parse the fields
    	if err := r.ParseForm(); err != nil {
    		s.tokenErrHelper(w, errInvalidRequest, "Couldn't parse data", http.StatusBadRequest)
    		return
    	}
    	q := r.Form
    
    	nonce := q.Get("nonce")
    	// Some clients, like the old go-oidc, provide extra whitespace. Tolerate this.
    	scopes := strings.Fields(q.Get("scope"))
    
    	// Parse the scopes if they are passed
    	var (
    		unrecognized  []string
    		invalidScopes []string
    	)
    	hasOpenIDScope := false
    	for _, scope := range scopes {
    		switch scope {
    		case scopeOpenID:
    			hasOpenIDScope = true
    		case scopeOfflineAccess, scopeEmail, scopeProfile, scopeGroups, scopeFederatedID:
    		default:
    			peerID, ok := parseCrossClientScope(scope)
    			if !ok {
    				unrecognized = append(unrecognized, scope)
    				continue
    			}
    
    
    			isTrusted, err := s.validateCrossClientTrust(client.ID, peerID)
    
    			if err != nil {
    				s.tokenErrHelper(w, errInvalidClient, fmt.Sprintf("Error validating cross client trust %v.", err), http.StatusBadRequest)
    				return
    			}
    			if !isTrusted {
    				invalidScopes = append(invalidScopes, scope)
    			}
    		}
    	}
    	if !hasOpenIDScope {
    		s.tokenErrHelper(w, errInvalidRequest, `Missing required scope(s) ["openid"].`, http.StatusBadRequest)
    		return
    	}
    	if len(unrecognized) > 0 {
    		s.tokenErrHelper(w, errInvalidRequest, fmt.Sprintf("Unrecognized scope(s) %q", unrecognized), http.StatusBadRequest)
    		return
    	}
    	if len(invalidScopes) > 0 {
    		s.tokenErrHelper(w, errInvalidRequest, fmt.Sprintf("Client can't request scope(s) %q", invalidScopes), http.StatusBadRequest)
    		return
    	}
    
    	// Which connector
    	connID := s.passwordConnector
    	conn, err := s.getConnector(connID)
    	if err != nil {
    		s.tokenErrHelper(w, errInvalidRequest, "Requested connector does not exist.", http.StatusBadRequest)
    		return
    	}
    
    	passwordConnector, ok := conn.Connector.(connector.PasswordConnector)
    	if !ok {
    		s.tokenErrHelper(w, errInvalidRequest, "Requested password connector does not correct type.", http.StatusBadRequest)
    		return
    	}
    
    	// Login
    	username := q.Get("username")
    	password := q.Get("password")
    	identity, ok, err := passwordConnector.Login(r.Context(), parseScopes(scopes), username, password)
    	if err != nil {
    
    		s.logger.Errorf("Failed to login user: %v", err)
    
    		s.tokenErrHelper(w, errInvalidRequest, "Could not login user", http.StatusBadRequest)
    		return
    	}
    	if !ok {
    		s.tokenErrHelper(w, errAccessDenied, "Invalid username or password", http.StatusUnauthorized)
    		return
    	}
    
    	// Build the claims to send the id token
    	claims := storage.Claims{
    		UserID:            identity.UserID,
    		Username:          identity.Username,
    		PreferredUsername: identity.PreferredUsername,
    		Email:             identity.Email,
    		EmailVerified:     identity.EmailVerified,
    		Groups:            identity.Groups,
    	}
    
    	accessToken := storage.NewID()
    
    	idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, nonce, accessToken, "", connID)
    
    	if err != nil {
    		s.tokenErrHelper(w, errServerError, fmt.Sprintf("failed to create ID token: %v", err), http.StatusInternalServerError)
    		return
    	}
    
    	reqRefresh := func() bool {
    		// Ensure the connector supports refresh tokens.
    		//
    		// Connectors like `saml` do not implement RefreshConnector.
    		_, ok := conn.Connector.(connector.RefreshConnector)
    		if !ok {
    			return false
    		}
    
    		for _, scope := range scopes {
    			if scope == scopeOfflineAccess {
    				return true
    			}
    		}
    		return false
    	}()
    	var refreshToken string
    	if reqRefresh {
    		refresh := storage.RefreshToken{
    			ID:          storage.NewID(),
    			Token:       storage.NewID(),
    
    			ClientID:    client.ID,
    
    			ConnectorID: connID,
    			Scopes:      scopes,
    			Claims:      claims,
    			Nonce:       nonce,
    			// ConnectorData: authCode.ConnectorData,
    			CreatedAt: s.now(),
    			LastUsed:  s.now(),
    		}
    		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
    		}
    
    		if err := s.storage.CreateRefresh(refresh); err != nil {
    			s.logger.Errorf("failed to create refresh token: %v", err)
    			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
    			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 {
    
    					if err == storage.ErrNotFound {
    						s.logger.Warnf("database inconsistent, refresh token missing: %v", oldTokenRef.ID)
    					} else {
    						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
    			}
    		}
    	}
    
    
    	resp := s.toAccessTokenResponse(idToken, accessToken, refreshToken, expiry)
    	s.writeAccessToken(w, resp)
    
    Josh Soref's avatar
    Josh Soref committed
    type accessTokenResponse 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"`
    }
    
    
    Josh Soref's avatar
    Josh Soref committed
    func (s *Server) toAccessTokenResponse(idToken, accessToken, refreshToken string, expiry time.Time) *accessTokenResponse {
    	return &accessTokenResponse{
    
    		accessToken,
    
    Eric Chiang's avatar
    Eric Chiang committed
    		"bearer",
    
    		int(expiry.Sub(s.now()).Seconds()),
    
    Eric Chiang's avatar
    Eric Chiang committed
    		refreshToken,
    		idToken,
    	}
    
    Josh Soref's avatar
    Josh Soref committed
    func (s *Server) writeAccessToken(w http.ResponseWriter, resp *accessTokenResponse) {
    
    Eric Chiang's avatar
    Eric Chiang committed
    	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)))
    
    
    	// Token response must include cache headers https://tools.ietf.org/html/rfc6749#section-5.1
    	w.Header().Set("Cache-Control", "no-store")
    	w.Header().Set("Pragma", "no-cache")
    
    Eric Chiang's avatar
    Eric Chiang committed
    	w.Write(data)
    }
    
    
    func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int, description string) {
    	if err := s.templates.err(r, w, status, description); err != nil {
    
    		s.logger.Errorf("Server template error: %v", err)
    	}
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    
    func (s *Server) tokenErrHelper(w http.ResponseWriter, typ string, description string, statusCode int) {
    	if err := tokenErr(w, typ, description, statusCode); err != nil {
    
    		s.logger.Errorf("token error response: %v", err)
    
    
    // Check for username prompt override from connector. Defaults to "Username".
    func usernamePrompt(conn connector.PasswordConnector) string {
    	if attr := conn.Prompt(); attr != "" {
    		return attr
    	}
    	return "Username"
    }