2022-01-07 23:04:58 +00:00
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
2020-12-01 21:25:12 +00:00
// SPDX-License-Identifier: Apache-2.0
// Package token provides a handler for the OIDC token endpoint.
package token
import (
2021-10-13 19:31:20 +00:00
"context"
2021-12-14 19:59:52 +00:00
"errors"
2020-12-01 21:25:12 +00:00
"net/http"
"github.com/ory/fosite"
2021-10-13 19:31:20 +00:00
"github.com/ory/x/errorsx"
2022-01-13 02:05:10 +00:00
"golang.org/x/oauth2"
2020-12-01 21:25:12 +00:00
"go.pinniped.dev/internal/httputil/httperr"
2020-12-04 15:06:55 +00:00
"go.pinniped.dev/internal/oidc"
2021-10-13 19:31:20 +00:00
"go.pinniped.dev/internal/oidc/provider"
2020-12-01 21:25:12 +00:00
"go.pinniped.dev/internal/plog"
2021-10-06 22:28:13 +00:00
"go.pinniped.dev/internal/psession"
2022-01-12 22:28:52 +00:00
"go.pinniped.dev/pkg/oidcclient/oidctypes"
2020-12-01 21:25:12 +00:00
)
2021-10-13 19:31:20 +00:00
var (
errMissingUpstreamSessionInternalError = & fosite . RFC6749Error {
ErrorField : "error" ,
DescriptionField : "There was an internal server error." ,
HintField : "Required upstream data not found in session." ,
CodeField : http . StatusInternalServerError ,
}
errUpstreamRefreshError = & fosite . RFC6749Error {
ErrorField : "error" ,
DescriptionField : "Error during upstream refresh." ,
CodeField : http . StatusUnauthorized ,
}
)
2020-12-01 21:25:12 +00:00
func NewHandler (
2021-10-13 19:31:20 +00:00
idpLister oidc . UpstreamIdentityProvidersLister ,
2020-12-01 21:25:12 +00:00
oauthHelper fosite . OAuth2Provider ,
) http . Handler {
return httperr . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) error {
2021-10-06 22:28:13 +00:00
session := psession . NewPinnipedSession ( )
accessRequest , err := oauthHelper . NewAccessRequest ( r . Context ( ) , r , session )
2020-12-01 21:25:12 +00:00
if err != nil {
2020-12-04 15:06:55 +00:00
plog . Info ( "token request error" , oidc . FositeErrorForLog ( err ) ... )
2020-12-01 21:25:12 +00:00
oauthHelper . WriteAccessError ( w , accessRequest , err )
return nil
}
2021-10-13 19:31:20 +00:00
// Check if we are performing a refresh grant.
if accessRequest . GetGrantTypes ( ) . ExactOne ( "refresh_token" ) {
// The above call to NewAccessRequest has loaded the session from storage into the accessRequest variable.
// The session, requested scopes, and requested audience from the original authorize request was retrieved
// from the Kube storage layer and added to the accessRequest. Additionally, the audience and scopes may
// have already been granted on the accessRequest.
err = upstreamRefresh ( r . Context ( ) , accessRequest , idpLister )
if err != nil {
plog . Info ( "upstream refresh error" , oidc . FositeErrorForLog ( err ) ... )
oauthHelper . WriteAccessError ( w , accessRequest , err )
return nil
}
}
2020-12-01 21:25:12 +00:00
accessResponse , err := oauthHelper . NewAccessResponse ( r . Context ( ) , accessRequest )
if err != nil {
2020-12-04 15:06:55 +00:00
plog . Info ( "token response error" , oidc . FositeErrorForLog ( err ) ... )
2020-12-01 21:25:12 +00:00
oauthHelper . WriteAccessError ( w , accessRequest , err )
return nil
}
oauthHelper . WriteAccessResponse ( w , accessRequest , accessResponse )
return nil
} )
}
2021-10-13 19:31:20 +00:00
func upstreamRefresh ( ctx context . Context , accessRequest fosite . AccessRequester , providerCache oidc . UpstreamIdentityProvidersLister ) error {
session := accessRequest . GetSession ( ) . ( * psession . PinnipedSession )
2021-10-25 21:25:43 +00:00
2021-10-13 19:31:20 +00:00
customSessionData := session . Custom
if customSessionData == nil {
return errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
providerName := customSessionData . ProviderName
providerUID := customSessionData . ProviderUID
if providerUID == "" || providerName == "" {
return errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
switch customSessionData . ProviderType {
case psession . ProviderTypeOIDC :
2021-12-14 00:40:13 +00:00
return upstreamOIDCRefresh ( ctx , session , providerCache )
2021-10-13 19:31:20 +00:00
case psession . ProviderTypeLDAP :
2021-10-28 19:00:56 +00:00
return upstreamLDAPRefresh ( ctx , providerCache , session )
2021-10-13 19:31:20 +00:00
case psession . ProviderTypeActiveDirectory :
2021-10-28 19:00:56 +00:00
return upstreamLDAPRefresh ( ctx , providerCache , session )
2021-10-13 19:31:20 +00:00
default :
return errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
}
2021-12-14 00:40:13 +00:00
func upstreamOIDCRefresh ( ctx context . Context , session * psession . PinnipedSession , providerCache oidc . UpstreamIdentityProvidersLister ) error {
s := session . Custom
2022-01-13 02:05:10 +00:00
if s . OIDC == nil {
return errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
2022-01-12 22:28:52 +00:00
2022-01-13 02:05:10 +00:00
accessTokenStored := s . OIDC . UpstreamAccessToken != ""
refreshTokenStored := s . OIDC . UpstreamRefreshToken != ""
2022-01-12 22:28:52 +00:00
exactlyOneTokenStored := ( accessTokenStored || refreshTokenStored ) && ! ( accessTokenStored && refreshTokenStored )
if ! exactlyOneTokenStored {
2021-10-13 19:31:20 +00:00
return errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
p , err := findOIDCProviderByNameAndValidateUID ( s , providerCache )
if err != nil {
return err
}
plog . Debug ( "attempting upstream refresh request" ,
"providerName" , s . ProviderName , "providerType" , s . ProviderType , "providerUID" , s . ProviderUID )
2022-01-13 02:05:10 +00:00
var tokens * oauth2 . Token
if refreshTokenStored {
tokens , err = p . PerformRefresh ( ctx , s . OIDC . UpstreamRefreshToken )
if err != nil {
return errorsx . WithStack ( errUpstreamRefreshError . WithHint (
"Upstream refresh failed." ,
) . WithWrap ( err ) . WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
}
} else {
2022-01-12 22:28:52 +00:00
tokens = & oauth2 . Token { AccessToken : s . OIDC . UpstreamAccessToken }
2021-10-13 19:31:20 +00:00
}
// Upstream refresh may or may not return a new ID token. From the spec:
// "the response body is the Token Response of Section 3.1.3.3 except that it might not contain an id_token."
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
2022-01-13 02:05:10 +00:00
_ , hasIDTok := tokens . Extra ( "id_token" ) . ( string )
2021-12-14 00:40:13 +00:00
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at
// least some providers do not include one, so we skip the nonce validation here (but not other validations).
2022-01-13 02:05:10 +00:00
validatedTokens , err := p . ValidateTokenAndMergeWithUserInfo ( ctx , tokens , "" , hasIDTok , accessTokenStored )
2021-12-14 00:40:13 +00:00
if err != nil {
return errorsx . WithStack ( errUpstreamRefreshError . WithHintf (
"Upstream refresh returned an invalid ID token or UserInfo response." ) . WithWrap ( err ) . WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
}
2022-01-12 22:28:52 +00:00
err = validateIdentityUnchangedSinceInitialLogin ( validatedTokens , session , p . GetUsernameClaim ( ) )
if err != nil {
return err
2021-10-13 19:31:20 +00:00
}
// Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in
// the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding
// overwriting the old one.
2022-01-13 02:05:10 +00:00
if tokens . RefreshToken != "" {
2022-01-12 22:28:52 +00:00
plog . Debug ( "upstream refresh request returned a new refresh token" ,
2021-10-13 19:31:20 +00:00
"providerName" , s . ProviderName , "providerType" , s . ProviderType , "providerUID" , s . ProviderUID )
2022-01-13 02:05:10 +00:00
s . OIDC . UpstreamRefreshToken = tokens . RefreshToken
2021-10-13 19:31:20 +00:00
}
return nil
}
2022-01-12 22:28:52 +00:00
func validateIdentityUnchangedSinceInitialLogin ( validatedTokens * oidctypes . Token , session * psession . PinnipedSession , usernameClaimName string ) error {
s := session . Custom
mergedClaims := validatedTokens . IDToken . Claims
// If we have any claims at all, we better have a subject, and it better match the previous value.
// but it's possible that we don't because both returning a new id token on refresh and having a userinfo
// endpoint are optional.
if len ( mergedClaims ) == 0 {
return nil
}
newSub , hasSub := getString ( mergedClaims , oidc . IDTokenSubjectClaim )
if ! hasSub {
return errorsx . WithStack ( errUpstreamRefreshError . WithHintf (
"Upstream refresh failed." ) . WithWrap ( errors . New ( "subject in upstream refresh not found" ) ) .
WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
}
if s . OIDC . UpstreamSubject != newSub {
return errorsx . WithStack ( errUpstreamRefreshError . WithHintf (
"Upstream refresh failed." ) . WithWrap ( errors . New ( "subject in upstream refresh does not match previous value" ) ) .
WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
}
newUsername , hasUsername := getString ( mergedClaims , usernameClaimName )
oldUsername := session . Fosite . Claims . Extra [ oidc . DownstreamUsernameClaim ]
// It's possible that a username wasn't returned by the upstream provider during refresh,
// but if it is, verify that it hasn't changed.
if hasUsername && oldUsername != newUsername {
return errorsx . WithStack ( errUpstreamRefreshError . WithHintf (
"Upstream refresh failed." ) . WithWrap ( errors . New ( "username in upstream refresh does not match previous value" ) ) .
WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
}
newIssuer , hasIssuer := getString ( mergedClaims , oidc . IDTokenIssuerClaim )
// It's possible that an issuer wasn't returned by the upstream provider during refresh,
// but if it is, verify that it hasn't changed.
if hasIssuer && s . OIDC . UpstreamIssuer != newIssuer {
return errorsx . WithStack ( errUpstreamRefreshError . WithHintf (
"Upstream refresh failed." ) . WithWrap ( errors . New ( "issuer in upstream refresh does not match previous value" ) ) .
WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
}
return nil
}
2022-01-07 23:04:58 +00:00
func getString ( m map [ string ] interface { } , key string ) ( string , bool ) {
val , ok := m [ key ] . ( string )
return val , ok
}
2021-10-13 19:31:20 +00:00
func findOIDCProviderByNameAndValidateUID (
s * psession . CustomSessionData ,
providerCache oidc . UpstreamIdentityProvidersLister ,
) ( provider . UpstreamOIDCIdentityProviderI , error ) {
for _ , p := range providerCache . GetOIDCIdentityProviders ( ) {
if p . GetName ( ) == s . ProviderName {
if p . GetResourceUID ( ) != s . ProviderUID {
2021-10-22 17:23:21 +00:00
return nil , errorsx . WithStack ( errUpstreamRefreshError . WithHint (
"Provider from upstream session data has changed its resource UID since authentication." ) )
2021-10-13 19:31:20 +00:00
}
return p , nil
}
}
return nil , errorsx . WithStack ( errUpstreamRefreshError .
2021-11-03 22:17:50 +00:00
WithHint ( "Provider from upstream session data was not found." ) . WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
2021-10-13 19:31:20 +00:00
}
2021-10-22 20:57:30 +00:00
2021-10-28 19:00:56 +00:00
func upstreamLDAPRefresh ( ctx context . Context , providerCache oidc . UpstreamIdentityProvidersLister , session * psession . PinnipedSession ) error {
username , err := getDownstreamUsernameFromPinnipedSession ( session )
if err != nil {
return err
}
subject := session . Fosite . Claims . Subject
s := session . Custom
2021-10-22 20:57:30 +00:00
// if you have neither a valid ldap session config nor a valid active directory session config
2021-11-03 17:33:22 +00:00
validLDAP := s . ProviderType == psession . ProviderTypeLDAP && s . LDAP != nil && s . LDAP . UserDN != ""
validAD := s . ProviderType == psession . ProviderTypeActiveDirectory && s . ActiveDirectory != nil && s . ActiveDirectory . UserDN != ""
if ! ( validLDAP || validAD ) {
2021-10-22 20:57:30 +00:00
return errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
2021-12-09 22:02:40 +00:00
var additionalAttributes map [ string ] string
2021-12-08 23:03:57 +00:00
if s . ProviderType == psession . ProviderTypeLDAP {
additionalAttributes = s . LDAP . ExtraRefreshAttributes
} else {
additionalAttributes = s . ActiveDirectory . ExtraRefreshAttributes
}
2021-10-22 20:57:30 +00:00
// get ldap/ad provider out of cache
2021-10-25 21:25:43 +00:00
p , dn , err := findLDAPProviderByNameAndValidateUID ( s , providerCache )
if err != nil {
return err
}
2021-10-28 19:00:56 +00:00
if session . IDTokenClaims ( ) . AuthTime . IsZero ( ) {
return errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
2021-10-22 20:57:30 +00:00
// run PerformRefresh
2021-10-28 19:00:56 +00:00
err = p . PerformRefresh ( ctx , provider . StoredRefreshAttributes {
2021-12-08 23:03:57 +00:00
Username : username ,
Subject : subject ,
DN : dn ,
AdditionalAttributes : additionalAttributes ,
2021-10-28 19:00:56 +00:00
} )
2021-10-22 20:57:30 +00:00
if err != nil {
2021-11-03 22:17:50 +00:00
return errorsx . WithStack ( errUpstreamRefreshError . WithHint (
"Upstream refresh failed." ) . WithWrap ( err ) . WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
2021-10-22 20:57:30 +00:00
}
return nil
}
func findLDAPProviderByNameAndValidateUID (
s * psession . CustomSessionData ,
providerCache oidc . UpstreamIdentityProvidersLister ,
) ( provider . UpstreamLDAPIdentityProviderI , string , error ) {
var providers [ ] provider . UpstreamLDAPIdentityProviderI
var dn string
if s . ProviderType == psession . ProviderTypeLDAP {
providers = providerCache . GetLDAPIdentityProviders ( )
dn = s . LDAP . UserDN
} else if s . ProviderType == psession . ProviderTypeActiveDirectory {
providers = providerCache . GetActiveDirectoryIdentityProviders ( )
dn = s . ActiveDirectory . UserDN
}
for _ , p := range providers {
if p . GetName ( ) == s . ProviderName {
if p . GetResourceUID ( ) != s . ProviderUID {
2021-11-03 22:17:50 +00:00
return nil , "" , errorsx . WithStack ( errUpstreamRefreshError . WithHint (
"Provider from upstream session data has changed its resource UID since authentication." ) . WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
2021-10-22 20:57:30 +00:00
}
return p , dn , nil
}
}
return nil , "" , errorsx . WithStack ( errUpstreamRefreshError .
2021-11-03 22:17:50 +00:00
WithHint ( "Provider from upstream session data was not found." ) . WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
2021-10-22 20:57:30 +00:00
}
2021-10-27 00:03:16 +00:00
func getDownstreamUsernameFromPinnipedSession ( session * psession . PinnipedSession ) ( string , error ) {
extra := session . Fosite . Claims . Extra
if extra == nil {
return "" , errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
downstreamUsernameInterface := extra [ "username" ]
if downstreamUsernameInterface == nil {
return "" , errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
2021-11-03 17:33:22 +00:00
downstreamUsername , ok := downstreamUsernameInterface . ( string )
if ! ok || len ( downstreamUsername ) == 0 {
return "" , errorsx . WithStack ( errMissingUpstreamSessionInternalError )
}
2021-10-27 00:03:16 +00:00
return downstreamUsername , nil
}