2021-10-06 22:28:13 +00:00
// Copyright 2020-2021 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"
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"
2021-12-14 00:40:13 +00:00
"go.pinniped.dev/internal/upstreamoidc"
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
2021-10-13 19:31:20 +00:00
if s . OIDC == nil || s . OIDC . UpstreamRefreshToken == "" {
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 )
refreshedTokens , err := p . PerformRefresh ( ctx , s . OIDC . UpstreamRefreshToken )
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-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
_ , hasIDTok := refreshedTokens . 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).
validatedTokens , err := p . ValidateToken ( ctx , refreshedTokens , "" , hasIDTok )
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 ) )
}
claims := 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 refresh token on refresh and having a userinfo
// endpoint are optional.
if len ( validatedTokens . IDToken . Claims ) != 0 {
newSub := claims [ "sub" ]
oldDownstreamSubject := session . Fosite . Claims . Subject
2021-12-14 23:27:08 +00:00
oldIss , oldSub , err := upstreamoidc . ExtractUpstreamSubjectAndIssuerFromDownstream ( oldDownstreamSubject )
2021-10-13 19:31:20 +00:00
if err != nil {
2021-12-14 19:59:52 +00:00
return errorsx . WithStack ( errUpstreamRefreshError . WithHintf ( "Upstream refresh failed." ) .
WithWrap ( err ) . WithDebugf ( "provider name: %q, provider type: %q" , s . ProviderName , s . ProviderType ) )
2021-12-14 00:40:13 +00:00
}
if oldSub != newSub {
return errorsx . WithStack ( errUpstreamRefreshError . WithHintf (
2021-12-14 19:59:52 +00:00
"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 ) )
}
usernameClaim := p . GetUsernameClaim ( )
newUsername := claims [ usernameClaim ]
2021-12-14 23:27:08 +00:00
oldUsername := session . Fosite . Claims . Extra [ "username" ]
2021-12-14 19:59:52 +00:00
// its possible this won't be returned.
// but if it is, verify that it hasn't changed.
2021-12-14 23:27:08 +00:00
if newUsername != nil && 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 := claims [ "iss" ]
if newIssuer != nil && oldIss != 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 ) )
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.
if refreshedTokens . RefreshToken != "" {
plog . Debug ( "upstream refresh request did not return a new refresh token" ,
"providerName" , s . ProviderName , "providerType" , s . ProviderType , "providerUID" , s . ProviderUID )
s . OIDC . UpstreamRefreshToken = refreshedTokens . RefreshToken
}
return nil
}
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
}