2023-05-08 21:07:38 +00:00
|
|
|
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
|
2021-10-06 22:28:13 +00:00
|
|
|
// 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"
|
2021-10-08 22:48:21 +00:00
|
|
|
"k8s.io/apimachinery/pkg/types"
|
2021-10-06 22:28:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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.
|
2021-10-08 22:48:21 +00:00
|
|
|
Custom *CustomSessionData `json:"custom,omitempty"`
|
2021-10-06 22:28:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var _ openid.Session = &PinnipedSession{}
|
|
|
|
|
2021-10-08 22:48:21 +00:00
|
|
|
// CustomSessionData is the custom session data needed by Pinniped. It should be treated as a union type,
|
2021-10-06 22:28:13 +00:00
|
|
|
// where the value of ProviderType decides which other fields to use.
|
2021-10-08 22:48:21 +00:00
|
|
|
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"`
|
|
|
|
|
2023-05-08 21:07:38 +00:00
|
|
|
// UpstreamUsername is the username from the upstream identity provider during the user's initial login before
|
|
|
|
// identity transformations were applied. We store this so that we can still reapply identity transformations
|
|
|
|
// during refresh flows even when an upstream OIDC provider does not return the username again during the upstream
|
|
|
|
// refresh, and so we can validate that same untransformed username was found during an LDAP refresh.
|
|
|
|
UpstreamUsername string `json:"upstreamUsername"`
|
|
|
|
|
|
|
|
// UpstreamGroups is the groups list from the upstream identity provider during the user's initial login before
|
|
|
|
// identity transformations were applied. We store this so that we can still reapply identity transformations
|
|
|
|
// during refresh flows even when an OIDC provider does not return the groups again during the upstream
|
|
|
|
// refresh, and when the LDAP search was configured to skip group refreshes.
|
|
|
|
UpstreamGroups []string `json:"upstreamGroups"`
|
|
|
|
|
2021-10-06 22:28:13 +00:00
|
|
|
// 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.
|
2021-10-08 22:48:21 +00:00
|
|
|
ProviderUID types.UID `json:"providerUID"`
|
2021-10-06 22:28:13 +00:00
|
|
|
|
|
|
|
// 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.
|
2023-05-08 21:07:38 +00:00
|
|
|
// Also used by the session storage garbage collector to decide which upstream to use for token revocation.
|
2021-10-06 22:28:13 +00:00
|
|
|
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.
|
2023-05-08 21:07:38 +00:00
|
|
|
// Also used to decide which of the pointer types below should be used.
|
2021-10-08 22:48:21 +00:00
|
|
|
ProviderType ProviderType `json:"providerType"`
|
2021-10-06 22:28:13 +00:00
|
|
|
|
2022-01-18 23:34:19 +00:00
|
|
|
// 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"`
|
|
|
|
|
2021-10-06 22:28:13 +00:00
|
|
|
// Only used when ProviderType == "oidc".
|
|
|
|
OIDC *OIDCSessionData `json:"oidc,omitempty"`
|
2021-10-22 20:57:30 +00:00
|
|
|
|
2023-05-08 21:07:38 +00:00
|
|
|
// Only used when ProviderType == "ldap".
|
2021-10-22 20:57:30 +00:00
|
|
|
LDAP *LDAPSessionData `json:"ldap,omitempty"`
|
|
|
|
|
2023-05-08 21:07:38 +00:00
|
|
|
// Only used when ProviderType == "activedirectory".
|
2021-10-22 20:57:30 +00:00
|
|
|
ActiveDirectory *ActiveDirectorySessionData `json:"activedirectory,omitempty"`
|
2021-10-06 22:28:13 +00:00
|
|
|
}
|
|
|
|
|
2021-10-08 22:48:21 +00:00
|
|
|
type ProviderType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
ProviderTypeOIDC ProviderType = "oidc"
|
|
|
|
ProviderTypeLDAP ProviderType = "ldap"
|
|
|
|
ProviderTypeActiveDirectory ProviderType = "activedirectory"
|
|
|
|
)
|
|
|
|
|
2021-10-06 22:28:13 +00:00
|
|
|
// OIDCSessionData is the additional data needed by Pinniped when the upstream IDP is an OIDC provider.
|
|
|
|
type OIDCSessionData struct {
|
2021-12-06 22:43:39 +00:00
|
|
|
// 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.
|
2021-10-06 22:28:13 +00:00
|
|
|
UpstreamRefreshToken string `json:"upstreamRefreshToken"`
|
2021-12-06 22:43:39 +00:00
|
|
|
|
|
|
|
// 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"`
|
2022-01-11 01:03:31 +00:00
|
|
|
|
|
|
|
// 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.
|
2021-12-06 22:43:39 +00:00
|
|
|
UpstreamSubject string `json:"upstreamSubject"`
|
2022-01-11 01:03:31 +00:00
|
|
|
|
|
|
|
// 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"`
|
2021-10-06 22:28:13 +00:00
|
|
|
}
|
|
|
|
|
2021-10-22 20:57:30 +00:00
|
|
|
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
|
|
|
|
type LDAPSessionData struct {
|
2021-12-09 22:02:40 +00:00
|
|
|
UserDN string `json:"userDN"`
|
|
|
|
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
|
2021-10-22 20:57:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
|
|
|
|
type ActiveDirectorySessionData struct {
|
2021-12-09 22:02:40 +00:00
|
|
|
UserDN string `json:"userDN"`
|
|
|
|
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
|
2021-10-22 20:57:30 +00:00
|
|
|
}
|
|
|
|
|
2021-10-06 22:28:13 +00:00
|
|
|
// NewPinnipedSession returns a new empty session.
|
|
|
|
func NewPinnipedSession() *PinnipedSession {
|
|
|
|
return &PinnipedSession{
|
|
|
|
Fosite: &openid.DefaultSession{
|
|
|
|
Claims: &jwt.IDTokenClaims{},
|
|
|
|
Headers: &jwt.Headers{},
|
|
|
|
},
|
2021-10-08 22:48:21 +00:00
|
|
|
Custom: &CustomSessionData{},
|
2021-10-06 22:28:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|