Files
kubesphere/pkg/kapis/oauth/handler.go
2025-04-30 15:53:51 +08:00

807 lines
30 KiB
Go

/*
* Copyright 2024 the KubeSphere Authors.
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package oauth
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/emicklei/go-restful/v3"
"github.com/go-jose/go-jose/v4"
"github.com/golang-jwt/jwt/v4"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog/v2"
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/apiserver/query"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/apiserver/rest"
"kubesphere.io/kubesphere/pkg/models/auth"
"kubesphere.io/kubesphere/pkg/models/iam/im"
serverrors "kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
)
const (
KindTokenReview = "TokenReview"
internalServerErrorMessage = "An internal server error occurred while processing the request."
)
type Spec struct {
Token string `json:"token" description:"access token"`
}
type Status struct {
Authenticated bool `json:"authenticated" description:"is authenticated"`
User map[string]interface{} `json:"user,omitempty" description:"user info"`
}
type TokenReview struct {
APIVersion string `json:"apiVersion" description:"Kubernetes API version"`
Kind string `json:"kind" description:"kind of the API object"`
Spec *Spec `json:"spec,omitempty"`
Status *Status `json:"status,omitempty" description:"token review status"`
}
type LoginRequest struct {
Username string `json:"username" description:"username"`
Password string `json:"password" description:"password"`
}
func (request *TokenReview) Validate() error {
if request.Spec == nil || request.Spec.Token == "" {
return fmt.Errorf("token must not be null")
}
return nil
}
// ProviderMetadata https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
type ProviderMetadata struct {
// URL using the https scheme with no query or fragment component that the OP
// asserts as its Issuer Identifier.
Issuer string `json:"issuer"`
// URL of the OP's OAuth 2.0 Authorization Endpoint.
Auth string `json:"authorization_endpoint"`
// URL of the OP's OAuth 2.0 Token Endpoint.
Token string `json:"token_endpoint"`
// URL of the OP's UserInfo Endpoint
UserInfo string `json:"userinfo_endpoint"`
// URL of the OP's JSON Web Key Set [JWK] document.
Keys string `json:"jwks_uri"`
// JSON array containing a list of the OAuth 2.0 Grant Type values that this OP supports.
GrantTypes []string `json:"grant_types_supported"`
// JSON array containing a list of the OAuth 2.0 response_type values that this OP supports.
ResponseTypes []string `json:"response_types_supported"`
// JSON array containing a list of the Subject Identifier types that this OP supports.
Subjects []string `json:"subject_types_supported"`
// JSON array containing a list of the JWS signing algorithms (alg values) supported by
// the OP for the ID Token to encode the Claims in a JWT [JWT].
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
// JSON array containing a list of Proof Key for Code
// Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server.
CodeChallengeAlgs []string `json:"code_challenge_methods_supported"`
// JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports.
Scopes []string `json:"scopes_supported"`
// JSON array containing a list of Client Authentication methods supported by this Token Endpoint.
AuthMethods []string `json:"token_endpoint_auth_methods_supported"`
// JSON array containing a list of the Claim Names of the Claims that the OpenID Provider
// MAY be able to supply values for.
Claims []string `json:"claims_supported"`
}
type handler struct {
im im.IdentityManagementInterface
options *authentication.Options
tokenOperator auth.TokenManagementInterface
passwordAuthenticator auth.PasswordAuthenticator
oauthAuthenticator auth.OAuthAuthenticator
loginRecorder auth.LoginRecorder
clientGetter oauth.ClientGetter
}
func NewHandler(im im.IdentityManagementInterface,
tokenOperator auth.TokenManagementInterface,
passwordAuthenticator auth.PasswordAuthenticator,
oauth2Authenticator auth.OAuthAuthenticator,
loginRecorder auth.LoginRecorder,
options *authentication.Options,
oauthOperator oauth.ClientGetter) rest.Handler {
handler := &handler{im: im,
tokenOperator: tokenOperator,
passwordAuthenticator: passwordAuthenticator,
oauthAuthenticator: oauth2Authenticator,
loginRecorder: loginRecorder,
options: options,
clientGetter: oauthOperator}
return handler
}
func FakeHandler() rest.Handler {
handler := &handler{}
return handler
}
// tokenReview Implement webhook authentication interface
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
func (h *handler) tokenReview(req *restful.Request, resp *restful.Response) {
var tokenReview TokenReview
err := req.ReadEntity(&tokenReview)
if err != nil {
api.HandleBadRequest(resp, req, err)
return
}
if err = tokenReview.Validate(); err != nil {
api.HandleBadRequest(resp, req, err)
return
}
verified, err := h.tokenOperator.Verify(tokenReview.Spec.Token)
if err != nil {
api.HandleBadRequest(resp, req, err)
return
}
authenticated := verified.User
success := TokenReview{APIVersion: tokenReview.APIVersion,
Kind: KindTokenReview,
Status: &Status{
Authenticated: true,
User: map[string]interface{}{"username": authenticated.GetName(), "uid": authenticated.GetUID()},
},
}
_ = resp.WriteEntity(success)
}
func (h *handler) discovery(_ *restful.Request, response *restful.Response) {
result := ProviderMetadata{
Issuer: h.options.Issuer.URL,
Auth: h.options.Issuer.URL + root + "/authorize",
Token: h.options.Issuer.URL + root + "/token",
Keys: h.options.Issuer.URL + root + "/keys",
UserInfo: h.options.Issuer.URL + root + "/userinfo",
Subjects: []string{"public"},
GrantTypes: []string{oauth.GrantTypeAuthorizationCode, oauth.GrantTypeRefreshToken},
IDTokenAlgs: []string{string(jose.RS256)},
CodeChallengeAlgs: []string{"plain", "S256"},
Scopes: []string{oauth.ScopeOpenID, oauth.ScopeEmail, oauth.ScopeProfile},
AuthMethods: []string{"client_secret_post"},
Claims: []string{
"iss", "sub", "aud", "iat", "exp", "email", "locale", "preferred_username",
},
ResponseTypes: []string{
oauth.ResponseTypeCode,
oauth.ResponseTypeIDToken,
},
}
_ = response.WriteAsJson(result)
}
func (h *handler) keys(_ *restful.Request, response *restful.Response) {
jwks := jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{*h.tokenOperator.Keys().SigningKeyPub},
}
_ = response.WriteEntity(jwks)
}
// The Authorization Endpoint performs Authentication of the End-User.
func (h *handler) authorize(req *restful.Request, response *restful.Response) {
var scope, responseType, clientID, redirectURI, state, nonce, prompt string
scope = req.QueryParameter("scope")
clientID = req.QueryParameter("client_id")
redirectURI = req.QueryParameter("redirect_uri")
responseType = req.QueryParameter("response_type")
state = req.QueryParameter("state")
nonce = req.QueryParameter("nonce")
prompt = req.QueryParameter("prompt")
// Authorization Servers MUST support the use of the HTTP GET and POST methods
// defined in RFC 2616 [RFC2616] at the Authorization Endpoint.
if req.Request.Method == http.MethodPost {
scope, _ = req.BodyParameter("scope")
clientID, _ = req.BodyParameter("client_id")
redirectURI, _ = req.BodyParameter("redirect_uri")
responseType, _ = req.BodyParameter("response_type")
state, _ = req.BodyParameter("state")
nonce, _ = req.BodyParameter("nonce")
prompt, _ = req.BodyParameter("prompt")
}
client, err := h.clientGetter.GetOAuthClient(req.Request.Context(), clientID)
if err != nil {
if errors.Is(err, oauth.ErrorClientNotFound) {
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidClient("The provided client_id is invalid or does not exist."))
return
}
klog.Errorf("failed to get oauth client: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
redirectURL, err := client.ResolveRedirectURL(redirectURI)
if err != nil {
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest("Redirect URI is not allowed."))
return
}
// Unless the Redirection URI is invalid, the Authorization Server returns the Client to the Redirection URI
// specified in the Authorization Request with the appropriate error and state parameters.
// Other parameters SHOULD NOT be returned.
// The authorization server informs the client by adding the following
// parameters to the query component of the redirection URI using the
// "application/x-www-form-urlencoded" format
informsError := func(err *oauth.Error) {
values := make(url.Values)
values.Add("error", string(err.Type))
if err.Description != "" {
values.Add("error_description", err.Description)
}
if state != "" {
values.Add("state", state)
}
redirectURL.RawQuery = values.Encode()
http.Redirect(response.ResponseWriter, req.Request, redirectURL.String(), http.StatusFound)
}
// If no openid scope value is present, the request may still be a valid OAuth 2.0 request,
// but is not an OpenID Connect request.
var scopes []string
if scope != "" {
scopes = strings.Split(scope, " ")
}
var responseTypes []string
if responseType != "" {
responseTypes = strings.Split(responseType, " ")
}
if !client.IsValidScope(scope) {
informsError(oauth.NewInvalidScope("The requested scope is invalid or not supported."))
return
}
// Hybrid flow is not supported now
if len(responseTypes) > 1 || !oauth.IsValidResponseTypes(responseTypes) {
informsError(oauth.NewError(oauth.UnsupportedResponseType, fmt.Sprintf("The provided response_type %s is not supported by the authorization server.", responseType)))
return
}
if client.GrantMethod == oauth.GrantMethodDeny {
informsError(oauth.NewInvalidGrant("The resource owner or authorization server denied the request."))
return
}
authenticated, _ := request.UserFrom(req.Request.Context())
if authenticated == nil || authenticated.GetName() == user.Anonymous {
if prompt == "none" {
informsError(oauth.NewError(oauth.LoginRequired, "Not authenticated."))
return
}
// TODO redirect to login page with refer
http.Redirect(response.ResponseWriter, req.Request, h.options.Issuer.URL, http.StatusFound)
return
}
approved := client.GrantMethod == oauth.GrantMethodAuto
if prompt == "none" && !approved {
informsError(oauth.NewError(oauth.InteractionRequired, "Consent is required before proceeding with the request."))
return
}
// TODO oauth.GrantMethodPrompt
// oauth.GrantMethodAuto
switch responseType {
case oauth.ResponseTypeCode:
h.handleAuthorizationCodeRequest(req, response, authCodeRequest{
authenticated: authenticated,
clientID: clientID,
nonce: nonce,
scopes: scopes,
redirectURL: redirectURL,
state: state,
})
case oauth.ResponseTypeIDToken:
h.handleAuthIDTokenRequest(req, response, &authIDTokenRequest{
idTokenRequest: &idTokenRequest{
authenticated: authenticated,
client: client,
nonce: nonce,
scopes: scopes,
},
redirectURL: redirectURL,
state: state,
})
default:
informsError(oauth.NewError(oauth.UnsupportedResponseType, "The provided response_type is not supported by the authorization server."))
}
}
func (h *handler) oauthCallback(req *restful.Request, response *restful.Response) {
provider := req.PathParameter("callback")
authenticated, err := h.oauthAuthenticator.Authenticate(req.Request.Context(), provider, req.Request)
if err != nil {
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
return
}
// TODO(@hongming) using the really client configuration
result, err := h.issueTokenTo(authenticated, nil)
if err != nil {
klog.Errorf("failed to issue token: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
if err = h.loginRecorder.RecordLogin(req.Request.Context(), authenticated.GetName(), iamv1beta1.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
}
_ = response.WriteEntity(result)
}
// token handles the Token Request to obtain an Access Token, an ID Token, and optionally a Refresh Token.
// This is used in the Authorization Code Flow, where the RP (Client) sends a Token Request to the Token Endpoint
// (described in Section 3.2 of OAuth 2.0 [RFC6749]) to obtain a Token Response.
// Communication with the Token Endpoint is required to utilize TLS for security.
func (h *handler) token(req *restful.Request, response *restful.Response) {
clientID, _ := req.BodyParameter("client_id")
clientSecret, _ := req.BodyParameter("client_secret")
grantType, _ := req.BodyParameter("grant_type")
// All Token Responses containing sensitive information MUST include the following HTTP response header fields and values:
// Cache-Control: no-store
// Pragma: no-cache
response.Header().Set("Cache-Control", "no-store")
response.Header().Set("Pragma", "no-cache")
// Retrieve the OAuth client associated with the provided client_id.
client, err := h.clientGetter.GetOAuthClient(req.Request.Context(), clientID)
if err != nil {
if errors.Is(err, oauth.ErrorClientNotFound) {
klog.Warningf("The provided client_id %s is invalid or does not exist.", clientID)
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidClient("The provided client_id is invalid or does not exist."))
return
}
klog.Errorf("failed to get oauth client: %v", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
// Check if the client_secret matches the one associated with the retrieved client.
if client.Secret != clientSecret {
klog.Warningf("Invalid client credential for client_id %s", clientID)
_ = response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.NewError(oauth.UnauthorizedClient, "Invalid client credential."))
return
}
unsupportedGrantType := oauth.NewError(oauth.UnsupportedGrantType, "The provided grant_type is not supported.")
switch grantType {
case oauth.GrantTypePassword:
if client.Trusted {
h.passwordGrant(req, response, client)
return
}
klog.Warningf("The client %s is not trusted.", client.Name)
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, unsupportedGrantType)
case oauth.GrantTypeRefreshToken:
h.refreshTokenGrant(req, response, client)
case oauth.GrantTypeCode, oauth.GrantTypeAuthorizationCode:
h.codeGrant(req, response, client)
default:
klog.Warningf("The provided grant_type %s is not supported.", grantType)
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, unsupportedGrantType)
}
}
// passwordGrant handles the Resource Owner Password Credentials Grant.
// For more details, refer to: https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
//
// The resource owner password credentials grant type is suitable in cases where
// the resource owner has a trust relationship with the client, such as the device
// operating system or a highly privileged application. The authorization server should
// take special care when enabling this grant type and only allow it when other flows
// are not viable.
func (h *handler) passwordGrant(req *restful.Request, response *restful.Response, client *oauth.Client) {
// Extracting parameters from the request body.
username, _ := req.BodyParameter("username")
password, _ := req.BodyParameter("password")
provider, _ := req.BodyParameter("provider")
// Authenticate the user credentials.
authenticated, err := h.passwordAuthenticator.Authenticate(req.Request.Context(), provider, username, password)
if err != nil {
switch {
case errors.Is(err, auth.AccountIsNotActiveError):
// The Account is suspended.
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("Account suspended."))
return
case errors.Is(err, auth.IncorrectPasswordError):
// Record unsuccessful login attempt.
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
if err := h.loginRecorder.RecordLogin(req.Request.Context(), username, iamv1beta1.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, err); err != nil {
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
}
// Invalid username or password.
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("Invalid username or password."))
return
case errors.Is(err, auth.RateLimitExceededError):
// Rate limit exceeded.
_ = response.WriteHeaderAndEntity(http.StatusTooManyRequests, oauth.NewInvalidGrant("Rate limit exceeded."))
return
default:
// Authentication failed.
klog.Errorf("Authentication failed: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
}
// Issue token to the authenticated user.
result, err := h.issueTokenTo(authenticated, client)
if err != nil {
// Failed to issue token.
klog.Errorf("Failed to issue token: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
// Record successful login.
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
if err = h.loginRecorder.RecordLogin(req.Request.Context(), authenticated.GetName(), iamv1beta1.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
}
// Respond with the issued token.
_ = response.WriteEntity(result)
}
func (h *handler) issueTokenTo(user user.Info, client *oauth.Client) (*oauth.Token, error) {
accessTokenMaxAge := h.options.Issuer.AccessTokenMaxAge
accessTokenInactivityTimeout := h.options.Issuer.AccessTokenInactivityTimeout
if client != nil && client.AccessTokenMaxAgeSeconds > 0 && client.AccessTokenInactivityTimeoutSeconds > 0 {
accessTokenMaxAge = time.Duration(client.AccessTokenMaxAgeSeconds) * time.Second
accessTokenInactivityTimeout = time.Duration(client.AccessTokenInactivityTimeoutSeconds) * time.Second
}
if !h.options.MultipleLogin {
if err := h.tokenOperator.RevokeAllUserTokens(user.GetName()); err != nil {
return nil, err
}
}
accessToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
User: user,
Claims: token.Claims{TokenType: token.AccessToken},
ExpiresIn: accessTokenMaxAge,
})
if err != nil {
return nil, err
}
refreshToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
User: user,
Claims: token.Claims{TokenType: token.RefreshToken},
ExpiresIn: accessTokenMaxAge + accessTokenInactivityTimeout,
})
if err != nil {
return nil, err
}
result := oauth.Token{
AccessToken: accessToken,
// The OAuth 2.0 token_type response parameter value MUST be Bearer,
// as specified in OAuth 2.0 Bearer Token Usage [RFC6750]
TokenType: "Bearer",
RefreshToken: refreshToken,
ExpiresIn: int(accessTokenMaxAge.Seconds()),
}
return &result, nil
}
func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Response, client *oauth.Client) {
refreshToken, _ := req.BodyParameter("refresh_token")
verified, err := h.tokenOperator.Verify(refreshToken)
if err != nil || verified.TokenType != token.RefreshToken {
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("The refresh token is invalid or expired."))
return
}
authenticated := verified.User
// update token after registration
if authenticated.GetName() == iamv1beta1.PreRegistrationUser &&
authenticated.GetExtra() != nil &&
len(authenticated.GetExtra()[iamv1beta1.ExtraIdentityProvider]) > 0 &&
len(authenticated.GetExtra()[iamv1beta1.ExtraUID]) > 0 {
idp := authenticated.GetExtra()[iamv1beta1.ExtraIdentityProvider][0]
uid := authenticated.GetExtra()[iamv1beta1.ExtraUID][0]
queryParam := query.New()
queryParam.Filters = map[query.Field]query.Value{query.FieldAnnotation: query.Value(fmt.Sprintf("%s.%s=%s", iamv1beta1.IdentityProviderAnnotation, idp, uid))}
users, err := h.im.ListUsers(queryParam)
if err != nil {
klog.Errorf("failed to list users: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
if len(users.Items) != 1 {
if len(users.Items) > 1 {
klog.Errorf("duplicate user IDs associated: %s/%s", idp, uid)
}
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("Authenticated user does not exist."))
return
}
authenticated = &user.DefaultInfo{Name: users.Items[0].(*iamv1beta1.User).Name}
}
result, err := h.issueTokenTo(authenticated, client)
if err != nil {
klog.Errorf("failed to issue token: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
_ = response.WriteEntity(result)
}
func (h *handler) codeGrant(req *restful.Request, response *restful.Response, client *oauth.Client) {
code, _ := req.BodyParameter("code")
if code == "" {
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest("The authorization code is empty or missing."))
return
}
redirectURI, _ := req.BodyParameter("redirect_uri")
if _, err := client.ResolveRedirectURL(redirectURI); err != nil {
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest("Redirect URI is not allowed."))
return
}
authorizeContext, err := h.tokenOperator.Verify(code)
if err != nil || authorizeContext.TokenType != token.AuthorizationCode {
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("The authorization code is invalid or expired."))
return
}
defer func() {
// The client MUST NOT use the authorization code more than once.
if err = h.tokenOperator.Revoke(code); err != nil {
klog.Warningf("grant: failed to revoke authorization code: %v", err)
}
}()
result, err := h.issueTokenTo(authorizeContext.User, client)
if err != nil {
klog.Errorf("failed to issue token: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
// If no openid scope value is present, the request may still be a valid OAuth 2.0 request,
// but is not an OpenID Connect request.
if !sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeOpenID) {
_ = response.WriteEntity(result)
return
}
idTokenRequest, err := h.buildIDTokenIssueRequest(&idTokenRequest{
authenticated: authorizeContext.User,
client: client,
scopes: authorizeContext.Scopes,
nonce: authorizeContext.Nonce,
})
if err != nil {
klog.Errorf("failed to build id token request: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
idToken, err := h.tokenOperator.IssueTo(idTokenRequest)
if err != nil {
klog.Errorf("failed to issue id token: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
result.IDToken = idToken
_ = response.WriteEntity(result)
}
func (h *handler) buildIDTokenIssueRequest(request *idTokenRequest) (*token.IssueRequest, error) {
authenticated, err := h.im.DescribeUser(request.authenticated.GetName())
if err != nil {
return nil, err
}
accessTokenMaxAge := h.options.Issuer.AccessTokenMaxAge
accessTokenInactivityTimeout := h.options.Issuer.AccessTokenInactivityTimeout
if request.client != nil && request.client.AccessTokenMaxAgeSeconds > 0 && request.client.AccessTokenInactivityTimeoutSeconds > 0 {
accessTokenMaxAge = time.Duration(request.client.AccessTokenMaxAgeSeconds) * time.Second
accessTokenInactivityTimeout = time.Duration(request.client.AccessTokenInactivityTimeoutSeconds) * time.Second
}
idTokenRequest := &token.IssueRequest{
User: request.authenticated,
Claims: token.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Audience: []string{request.client.Name},
},
Nonce: request.nonce,
TokenType: token.IDToken,
Name: request.authenticated.GetName(),
},
ExpiresIn: accessTokenMaxAge + accessTokenInactivityTimeout,
}
if sliceutil.HasString(request.scopes, oauth.ScopeProfile) {
idTokenRequest.PreferredUsername = authenticated.Name
idTokenRequest.Locale = authenticated.Spec.Lang
}
if sliceutil.HasString(request.scopes, oauth.ScopeEmail) {
idTokenRequest.Email = authenticated.Spec.Email
}
return idTokenRequest, nil
}
func (h *handler) logout(req *restful.Request, resp *restful.Response) {
authHeader := strings.TrimSpace(req.Request.Header.Get("Authorization"))
if authHeader == "" {
_ = resp.WriteAsJson(serverrors.None)
return
}
parts := strings.Split(authHeader, " ")
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
_ = resp.WriteAsJson(serverrors.None)
return
}
accessToken := parts[1]
if err := h.tokenOperator.Revoke(accessToken); err != nil {
reason := fmt.Errorf("failed to revoke access token")
klog.Errorf("%s: %s", reason, err)
api.HandleInternalError(resp, req, reason)
return
}
postLogoutRedirectURI := req.QueryParameter("post_logout_redirect_uri")
if postLogoutRedirectURI == "" {
_ = resp.WriteAsJson(serverrors.None)
return
}
redirectURL, err := url.Parse(postLogoutRedirectURI)
if err != nil {
api.HandleBadRequest(resp, req, fmt.Errorf("invalid logout redirect URI: %s", err))
return
}
state := req.QueryParameter("state")
if state != "" {
qry := redirectURL.Query()
qry.Add("state", state)
redirectURL.RawQuery = qry.Encode()
}
resp.Header().Set("Content-Type", "text/plain")
http.Redirect(resp, req.Request, redirectURL.String(), http.StatusFound)
}
// userinfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User.
func (h *handler) userinfo(req *restful.Request, response *restful.Response) {
authenticated, _ := request.UserFrom(req.Request.Context())
if authenticated == nil || authenticated.GetName() == user.Anonymous {
_ = response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.LoginRequired)
return
}
userDetails, err := h.im.DescribeUser(authenticated.GetName())
if err != nil {
klog.Errorf("failed to get user details: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
result := token.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: userDetails.Name,
},
Name: userDetails.Name,
Email: userDetails.Spec.Email,
Locale: userDetails.Spec.Lang,
PreferredUsername: userDetails.Name,
}
_ = response.WriteEntity(result)
}
type authCodeRequest struct {
authenticated user.Info
clientID string
nonce string
scopes []string
redirectURL *url.URL
state string
}
func (h *handler) handleAuthorizationCodeRequest(req *restful.Request, response *restful.Response, authCodeRequest authCodeRequest) {
code, err := h.tokenOperator.IssueTo(&token.IssueRequest{
User: authCodeRequest.authenticated,
Claims: token.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Audience: []string{authCodeRequest.clientID},
},
TokenType: token.AuthorizationCode,
Nonce: authCodeRequest.nonce,
Scopes: authCodeRequest.scopes,
},
// A maximum authorization code lifetime of 10 minutes is
ExpiresIn: 10 * time.Minute,
})
if err != nil {
klog.Errorf("failed to issue auth code: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
values := authCodeRequest.redirectURL.Query()
values.Add("code", code)
if authCodeRequest.state != "" {
values.Add("state", authCodeRequest.state)
}
authCodeRequest.redirectURL.RawQuery = values.Encode()
http.Redirect(response, req.Request, authCodeRequest.redirectURL.String(), http.StatusFound)
}
type idTokenRequest struct {
authenticated user.Info
client *oauth.Client
nonce string
scopes []string
}
type authIDTokenRequest struct {
*idTokenRequest
state string
redirectURL *url.URL
}
func (h *handler) handleAuthIDTokenRequest(req *restful.Request, response *restful.Response, authIDTokenRequest *authIDTokenRequest) {
if authIDTokenRequest.nonce == "" {
return
}
idTokenRequest, err := h.buildIDTokenIssueRequest(authIDTokenRequest.idTokenRequest)
if err != nil {
klog.Errorf("failed to build id token request: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
return
}
idToken, err := h.tokenOperator.IssueTo(idTokenRequest)
if err != nil {
klog.Errorf("failed to issue id token: %s", err)
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
}
values := make(url.Values)
values.Add("id_token", idToken)
if authIDTokenRequest.state != "" {
values.Add("state", authIDTokenRequest.state)
}
authIDTokenRequest.redirectURL.Fragment = values.Encode()
http.Redirect(response, req.Request, authIDTokenRequest.redirectURL.String(), http.StatusFound)
}