ContainerImage.Pinniped/internal/psession/pinniped_session.go

154 lines
6.2 KiB
Go
Raw Normal View History

// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package psession
import (
"time"
"github.com/mohae/deepcopy"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
"k8s.io/apimachinery/pkg/types"
)
// PinnipedSession is a session container which includes the fosite standard stuff plus custom Pinniped stuff.
type PinnipedSession struct {
// Delegate most things to the standard fosite OpenID JWT session.
Fosite *openid.DefaultSession `json:"fosite,omitempty"`
// Custom Pinniped extensions to the session data.
Custom *CustomSessionData `json:"custom,omitempty"`
}
var _ openid.Session = &PinnipedSession{}
// CustomSessionData is the custom session data needed by Pinniped. It should be treated as a union type,
// where the value of ProviderType decides which other fields to use.
type CustomSessionData struct {
Create username scope, required for clients to get username in ID token - For backwards compatibility with older Pinniped CLIs, the pinniped-cli client does not need to request the username or groups scopes for them to be granted. For dynamic clients, the usual OAuth2 rules apply: the client must be allowed to request the scopes according to its configuration, and the client must actually request the scopes in the authorization request. - If the username scope was not granted, then there will be no username in the ID token, and the cluster-scoped token exchange will fail since there would be no username in the resulting cluster-scoped ID token. - The OIDC well-known discovery endpoint lists the username and groups scopes in the scopes_supported list, and lists the username and groups claims in the claims_supported list. - Add username and groups scopes to the default list of scopes put into kubeconfig files by "pinniped get kubeconfig" CLI command, and the default list of scopes used by "pinniped login oidc" when no list of scopes is specified in the kubeconfig file - The warning header about group memberships changing during upstream refresh will only be sent to the pinniped-cli client, since it is only intended for kubectl and it could leak the username to the client (which may not have the username scope granted) through the warning message text. - Add the user's username to the session storage as a new field, so that during upstream refresh we can compare the original username from the initial authorization to the refreshed username, even in the case when the username scope was not granted (and therefore the username is not stored in the ID token claims of the session storage) - Bump the Supervisor session storage format version from 2 to 3 due to the username field being added to the session struct - Extract commonly used string constants related to OIDC flows to api package. - Change some import names to make them consistent: - Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc" - Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc as "oidcapi" - Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
// Username will contain the downstream username determined during initial authorization. We store this
// so that we can validate that it does not change upon refresh. This should normally never be empty, since
// all users must have a username.
Username string `json:"username"`
// The Kubernetes resource UID of the identity provider CRD for the upstream IDP used to start this session.
// This should be validated again upon downstream refresh to make sure that we are not refreshing against
// a different identity provider CRD which just happens to have the same name.
// This implies that when a user deletes an identity provider CRD, then the sessions that were started
// using that identity provider will not be able to perform any more downstream refreshes.
ProviderUID types.UID `json:"providerUID"`
// The Kubernetes resource name of the identity provider CRD for the upstream IDP used to start this session.
// Used during a downstream refresh to decide which upstream to refresh.
// Also used to decide which of the pointer types below should be used.
ProviderName string `json:"providerName"`
// The type of the identity provider for the upstream IDP used to start this session.
// Used during a downstream refresh to decide which upstream to refresh.
ProviderType ProviderType `json:"providerType"`
// Warnings that were encountered at some point during login that should be emitted to the client.
// These will be RFC 2616-formatted errors with error code 299.
Warnings []string `json:"warnings"`
// Only used when ProviderType == "oidc".
OIDC *OIDCSessionData `json:"oidc,omitempty"`
LDAP *LDAPSessionData `json:"ldap,omitempty"`
ActiveDirectory *ActiveDirectorySessionData `json:"activedirectory,omitempty"`
}
type ProviderType string
const (
ProviderTypeOIDC ProviderType = "oidc"
ProviderTypeLDAP ProviderType = "ldap"
ProviderTypeActiveDirectory ProviderType = "activedirectory"
)
// OIDCSessionData is the additional data needed by Pinniped when the upstream IDP is an OIDC provider.
type OIDCSessionData struct {
// UpstreamRefreshToken will contain the refresh token from the upstream OIDC provider, if the upstream provider
// returned a refresh token during initial authorization. Otherwise, this field should be empty
// and the UpstreamAccessToken should be non-empty. We may not get a refresh token from the upstream provider,
// but we should always get an access token. However, when we do get a refresh token there is no need to
// also store the access token, since storing unnecessary tokens makes it possible for them to be leaked and
// creates more upstream revocation HTTP requests when it comes time to revoke the stored tokens.
UpstreamRefreshToken string `json:"upstreamRefreshToken"`
// UpstreamAccessToken will contain the access token returned by the upstream OIDC provider during initial
// authorization, but only when the provider did not also return a refresh token. When UpstreamRefreshToken is
// non-empty, then this field should be empty, indicating that we should use the upstream refresh token during
// downstream refresh.
UpstreamAccessToken string `json:"upstreamAccessToken"`
// UpstreamSubject is the "sub" claim from the upstream identity provider from the user's initial login. We store this
// so that we can validate that it does not change upon refresh.
UpstreamSubject string `json:"upstreamSubject"`
// UpstreamIssuer is the "iss" claim from the upstream identity provider from the user's initial login. We store this
// so that we can validate that it does not change upon refresh.
UpstreamIssuer string `json:"upstreamIssuer"`
}
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
type LDAPSessionData struct {
UserDN string `json:"userDN"`
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
}
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
type ActiveDirectorySessionData struct {
UserDN string `json:"userDN"`
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
}
// NewPinnipedSession returns a new empty session.
func NewPinnipedSession() *PinnipedSession {
return &PinnipedSession{
Fosite: &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{},
Headers: &jwt.Headers{},
},
Custom: &CustomSessionData{},
}
}
func (s *PinnipedSession) Clone() fosite.Session {
// Implementation copied from openid.DefaultSession's clone method.
if s == nil {
return nil
}
return deepcopy.Copy(s).(fosite.Session)
}
func (s *PinnipedSession) SetExpiresAt(key fosite.TokenType, exp time.Time) {
s.Fosite.SetExpiresAt(key, exp)
}
func (s *PinnipedSession) GetExpiresAt(key fosite.TokenType) time.Time {
return s.Fosite.GetExpiresAt(key)
}
func (s *PinnipedSession) GetUsername() string {
return s.Fosite.GetUsername()
}
func (s *PinnipedSession) SetSubject(subject string) {
s.Fosite.SetSubject(subject)
}
func (s *PinnipedSession) GetSubject() string {
return s.Fosite.GetSubject()
}
func (s *PinnipedSession) IDTokenHeaders() *jwt.Headers {
return s.Fosite.IDTokenHeaders()
}
func (s *PinnipedSession) IDTokenClaims() *jwt.IDTokenClaims {
return s.Fosite.IDTokenClaims()
}