2022-01-11 01:03:31 +00:00
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
2020-10-06 22:27:36 +00:00
// SPDX-License-Identifier: Apache-2.0
2020-10-21 18:04:46 +00:00
package oidcclient
2020-10-06 22:27:36 +00:00
import (
2021-06-21 19:40:08 +00:00
"bytes"
2020-10-06 22:27:36 +00:00
"context"
"encoding/json"
2021-04-20 00:59:46 +00:00
"errors"
2020-10-06 22:27:36 +00:00
"fmt"
2021-04-20 00:59:46 +00:00
"io/ioutil"
2021-07-08 19:32:44 +00:00
"net"
2020-10-06 22:27:36 +00:00
"net/http"
"net/http/httptest"
"net/url"
2021-04-20 00:59:46 +00:00
"strings"
2021-07-08 19:32:44 +00:00
"syscall"
2020-10-06 22:27:36 +00:00
"testing"
"time"
2021-01-20 17:54:44 +00:00
"github.com/coreos/go-oidc/v3/oidc"
2020-10-06 22:27:36 +00:00
"github.com/golang/mock/gomock"
2020-10-22 21:12:02 +00:00
"github.com/stretchr/testify/assert"
2020-10-06 22:27:36 +00:00
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
2020-10-21 18:04:46 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021-04-16 17:46:59 +00:00
"k8s.io/klog/v2"
2020-10-06 22:27:36 +00:00
"go.pinniped.dev/internal/httputil/httperr"
2021-04-20 00:59:46 +00:00
"go.pinniped.dev/internal/httputil/roundtripper"
2020-11-30 23:14:57 +00:00
"go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider"
"go.pinniped.dev/internal/oidc/provider"
2020-11-20 01:57:07 +00:00
"go.pinniped.dev/internal/testutil"
2021-04-16 17:46:59 +00:00
"go.pinniped.dev/internal/testutil/testlogger"
2021-10-13 19:31:20 +00:00
"go.pinniped.dev/internal/upstreamoidc"
2020-11-17 18:46:54 +00:00
"go.pinniped.dev/pkg/oidcclient/nonce"
2020-11-30 23:02:03 +00:00
"go.pinniped.dev/pkg/oidcclient/oidctypes"
2020-11-17 18:46:54 +00:00
"go.pinniped.dev/pkg/oidcclient/pkce"
"go.pinniped.dev/pkg/oidcclient/state"
2020-10-06 22:27:36 +00:00
)
2020-10-21 18:05:19 +00:00
// mockSessionCache exists to avoid an import cycle if we generate mocks into another package.
type mockSessionCache struct {
t * testing . T
2020-11-30 23:02:03 +00:00
getReturnsToken * oidctypes . Token
2020-10-21 18:05:19 +00:00
sawGetKeys [ ] SessionCacheKey
sawPutKeys [ ] SessionCacheKey
2020-11-30 23:02:03 +00:00
sawPutTokens [ ] * oidctypes . Token
2020-10-21 18:05:19 +00:00
}
2020-11-30 23:02:03 +00:00
func ( m * mockSessionCache ) GetToken ( key SessionCacheKey ) * oidctypes . Token {
2020-10-21 18:05:19 +00:00
m . t . Logf ( "saw mock session cache GetToken() with client ID %s" , key . ClientID )
m . sawGetKeys = append ( m . sawGetKeys , key )
return m . getReturnsToken
}
2020-11-30 23:02:03 +00:00
func ( m * mockSessionCache ) PutToken ( key SessionCacheKey , token * oidctypes . Token ) {
2020-10-21 18:05:19 +00:00
m . t . Logf ( "saw mock session cache PutToken() with client ID %s and ID token %s" , key . ClientID , token . IDToken . Token )
m . sawPutKeys = append ( m . sawPutKeys , key )
m . sawPutTokens = append ( m . sawPutTokens , token )
}
2021-04-20 00:59:46 +00:00
func TestLogin ( t * testing . T ) { // nolint:gocyclo
2020-10-22 21:12:02 +00:00
time1 := time . Date ( 2035 , 10 , 12 , 13 , 14 , 15 , 16 , time . UTC )
time1Unix := int64 ( 2075807775 )
require . Equal ( t , time1Unix , time1 . Add ( 2 * time . Minute ) . Unix ( ) )
2020-11-30 23:02:03 +00:00
testToken := oidctypes . Token {
2020-11-30 23:14:57 +00:00
AccessToken : & oidctypes . AccessToken { Token : "test-access-token" , Expiry : metav1 . NewTime ( time1 . Add ( 1 * time . Minute ) ) } ,
RefreshToken : & oidctypes . RefreshToken { Token : "test-refresh-token" } ,
IDToken : & oidctypes . IDToken { Token : "test-id-token" , Expiry : metav1 . NewTime ( time1 . Add ( 2 * time . Minute ) ) } ,
2020-10-06 22:27:36 +00:00
}
2020-12-04 23:33:53 +00:00
testExchangedToken := oidctypes . Token {
IDToken : & oidctypes . IDToken { Token : "test-id-token-with-requested-audience" , Expiry : metav1 . NewTime ( time1 . Add ( 3 * time . Minute ) ) } ,
}
2020-10-06 22:27:36 +00:00
// Start a test server that returns 500 errors
errorServer := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
http . Error ( w , "some discovery error" , http . StatusInternalServerError )
} ) )
t . Cleanup ( errorServer . Close )
2021-06-21 19:19:12 +00:00
// Start a test server that returns discovery data with a broken response_modes_supported value.
brokenResponseModeMux := http . NewServeMux ( )
brokenResponseModeServer := httptest . NewServer ( brokenResponseModeMux )
brokenResponseModeMux . HandleFunc ( "/.well-known/openid-configuration" , func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "content-type" , "application/json" )
type providerJSON struct {
Issuer string ` json:"issuer" `
ResponseModesSupported string ` json:"response_modes_supported" ` // Wrong type (should be []string).
}
_ = json . NewEncoder ( w ) . Encode ( & providerJSON {
Issuer : brokenResponseModeServer . URL ,
ResponseModesSupported : "invalid" ,
} )
} )
t . Cleanup ( brokenResponseModeServer . Close )
2020-12-04 23:33:53 +00:00
// Start a test server that returns discovery data with a broken token URL
brokenTokenURLMux := http . NewServeMux ( )
brokenTokenURLServer := httptest . NewServer ( brokenTokenURLMux )
brokenTokenURLMux . HandleFunc ( "/.well-known/openid-configuration" , func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "content-type" , "application/json" )
type providerJSON struct {
Issuer string ` json:"issuer" `
AuthURL string ` json:"authorization_endpoint" `
TokenURL string ` json:"token_endpoint" `
JWKSURL string ` json:"jwks_uri" `
}
_ = json . NewEncoder ( w ) . Encode ( & providerJSON {
Issuer : brokenTokenURLServer . URL ,
AuthURL : brokenTokenURLServer . URL + "/authorize" ,
TokenURL : "%" ,
JWKSURL : brokenTokenURLServer . URL + "/keys" ,
} )
} )
t . Cleanup ( brokenTokenURLServer . Close )
2021-06-21 19:19:12 +00:00
discoveryHandler := func ( server * httptest . Server , responseModes [ ] string ) http . HandlerFunc {
return func ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
http . Error ( w , "unexpected method" , http . StatusMethodNotAllowed )
return
}
w . Header ( ) . Set ( "content-type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( & struct {
Issuer string ` json:"issuer" `
AuthURL string ` json:"authorization_endpoint" `
TokenURL string ` json:"token_endpoint" `
JWKSURL string ` json:"jwks_uri" `
ResponseModesSupported [ ] string ` json:"response_modes_supported,omitempty" `
} {
Issuer : server . URL ,
AuthURL : server . URL + "/authorize" ,
TokenURL : server . URL + "/token" ,
JWKSURL : server . URL + "/keys" ,
ResponseModesSupported : responseModes ,
} )
2020-10-06 22:27:36 +00:00
}
2021-06-21 19:19:12 +00:00
}
tokenHandler := func ( w http . ResponseWriter , r * http . Request ) {
2020-10-22 21:12:02 +00:00
if r . Method != http . MethodPost {
http . Error ( w , "unexpected method" , http . StatusMethodNotAllowed )
return
}
if err := r . ParseForm ( ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
var response struct {
oauth2 . Token
2020-12-04 23:33:53 +00:00
IDToken string ` json:"id_token,omitempty" `
ExpiresIn int64 ` json:"expires_in" `
IssuedTokenType string ` json:"issued_token_type,omitempty" `
2020-10-22 21:12:02 +00:00
}
2020-12-04 23:33:53 +00:00
switch r . Form . Get ( "grant_type" ) {
case "refresh_token" :
if r . Form . Get ( "client_id" ) != "test-client-id" {
http . Error ( w , "expected client_id 'test-client-id'" , http . StatusBadRequest )
return
}
response . AccessToken = testToken . AccessToken . Token
response . ExpiresIn = int64 ( time . Until ( testToken . AccessToken . Expiry . Time ) . Seconds ( ) )
response . RefreshToken = testToken . RefreshToken . Token
response . IDToken = testToken . IDToken . Token
if r . Form . Get ( "refresh_token" ) == "test-refresh-token-returning-invalid-id-token" {
response . IDToken = "not a valid JWT"
} else if r . Form . Get ( "refresh_token" ) != "test-refresh-token" {
http . Error ( w , "expected refresh_token to be 'test-refresh-token'" , http . StatusBadRequest )
return
}
case "urn:ietf:params:oauth:grant-type:token-exchange" :
2020-12-09 16:08:41 +00:00
if r . Form . Get ( "client_id" ) != "test-client-id" {
http . Error ( w , "bad client_id" , http . StatusBadRequest )
return
}
2020-12-04 23:33:53 +00:00
switch r . Form . Get ( "audience" ) {
case "test-audience-produce-invalid-http-response" :
http . Redirect ( w , r , "%" , http . StatusTemporaryRedirect )
return
case "test-audience-produce-http-400" :
http . Error ( w , "some server error" , http . StatusBadRequest )
return
2020-12-10 16:09:42 +00:00
case "test-audience-produce-invalid-content-type" :
w . Header ( ) . Set ( "content-type" , "invalid/invalid;=" )
return
2020-12-04 23:33:53 +00:00
case "test-audience-produce-wrong-content-type" :
w . Header ( ) . Set ( "content-type" , "invalid" )
return
case "test-audience-produce-invalid-json" :
2020-12-10 16:09:42 +00:00
w . Header ( ) . Set ( "content-type" , "application/json;charset=UTF-8" )
2020-12-04 23:33:53 +00:00
_ , _ = w . Write ( [ ] byte ( ` { ` ) )
return
case "test-audience-produce-invalid-tokentype" :
response . TokenType = "invalid"
case "test-audience-produce-invalid-issuedtokentype" :
response . TokenType = "N_A"
response . IssuedTokenType = "invalid"
case "test-audience-produce-invalid-jwt" :
response . TokenType = "N_A"
response . IssuedTokenType = "urn:ietf:params:oauth:token-type:jwt"
response . AccessToken = "not-a-valid-jwt"
default :
response . TokenType = "N_A"
response . IssuedTokenType = "urn:ietf:params:oauth:token-type:jwt"
response . AccessToken = testExchangedToken . IDToken . Token
}
default :
http . Error ( w , fmt . Sprintf ( "invalid grant_type %q" , r . Form . Get ( "grant_type" ) ) , http . StatusBadRequest )
2020-10-22 21:12:02 +00:00
return
}
w . Header ( ) . Set ( "content-type" , "application/json" )
require . NoError ( t , json . NewEncoder ( w ) . Encode ( & response ) )
2021-06-21 19:19:12 +00:00
}
// Start a test server that returns a real discovery document and answers refresh requests.
providerMux := http . NewServeMux ( )
successServer := httptest . NewServer ( providerMux )
t . Cleanup ( successServer . Close )
providerMux . HandleFunc ( "/.well-known/openid-configuration" , discoveryHandler ( successServer , nil ) )
providerMux . HandleFunc ( "/token" , tokenHandler )
// Start a test server that returns a real discovery document and answers refresh requests, _and_ supports form_mode=post.
formPostProviderMux := http . NewServeMux ( )
formPostSuccessServer := httptest . NewServer ( formPostProviderMux )
t . Cleanup ( formPostSuccessServer . Close )
formPostProviderMux . HandleFunc ( "/.well-known/openid-configuration" , discoveryHandler ( formPostSuccessServer , [ ] string { "query" , "form_post" } ) )
formPostProviderMux . HandleFunc ( "/token" , tokenHandler )
2020-10-06 22:27:36 +00:00
2021-08-19 17:20:24 +00:00
defaultDiscoveryResponse := func ( req * http . Request ) ( * http . Response , error ) {
2021-04-20 00:59:46 +00:00
// Call the handler function from the test server to calculate the response.
handler , _ := providerMux . Handler ( req )
recorder := httptest . NewRecorder ( )
handler . ServeHTTP ( recorder , req )
return recorder . Result ( ) , nil
}
2021-08-19 17:20:24 +00:00
defaultLDAPTestOpts := func ( t * testing . T , h * handlerState , authResponse * http . Response , authError error ) error {
2021-04-20 00:59:46 +00:00
h . generateState = func ( ) ( state . State , error ) { return "test-state" , nil }
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "test-pkce" , nil }
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "test-nonce" , nil }
2021-06-30 20:06:37 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) { return "some-upstream-username" , nil }
2021-07-29 22:49:16 +00:00
h . promptForSecret = func ( _ string ) ( string , error ) { return "some-upstream-password" , nil }
2021-04-20 00:59:46 +00:00
cache := & mockSessionCache { t : t , getReturnsToken : nil }
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
2021-05-13 17:05:56 +00:00
require . NoError ( t , WithCLISendingCredentials ( ) ( h ) )
2021-04-20 00:59:46 +00:00
require . NoError ( t , WithUpstreamIdentityProvider ( "some-upstream-name" , "ldap" ) ( h ) )
require . NoError ( t , WithClient ( & http . Client {
Transport : roundtripper . Func ( func ( req * http . Request ) ( * http . Response , error ) {
switch req . URL . Scheme + "://" + req . URL . Host + req . URL . Path {
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/.well-known/openid-configuration" :
return defaultDiscoveryResponse ( req )
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/authorize" :
return authResponse , authError
default :
require . FailNow ( t , fmt . Sprintf ( "saw unexpected http call from the CLI: %s" , req . URL . String ( ) ) )
return nil , nil
}
} ) ,
} ) ( h ) )
return nil
}
2020-10-06 22:27:36 +00:00
tests := [ ] struct {
name string
opt func ( t * testing . T ) Option
issuer string
clientID string
wantErr string
2020-11-30 23:02:03 +00:00
wantToken * oidctypes . Token
2021-04-16 17:46:59 +00:00
wantLogs [ ] string
2020-10-06 22:27:36 +00:00
} {
{
name : "option error" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
return fmt . Errorf ( "some option error" )
}
} ,
wantErr : "some option error" ,
} ,
{
name : "error generating state" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . generateState = func ( ) ( state . State , error ) { return "" , fmt . Errorf ( "some error generating state" ) }
return nil
}
} ,
wantErr : "some error generating state" ,
} ,
{
name : "error generating nonce" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "" , fmt . Errorf ( "some error generating nonce" ) }
return nil
}
} ,
wantErr : "some error generating nonce" ,
} ,
{
name : "error generating PKCE" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "" , fmt . Errorf ( "some error generating PKCE" ) }
return nil
}
} ,
wantErr : "some error generating PKCE" ,
} ,
2020-10-21 18:05:19 +00:00
{
name : "session cache hit but token expired" ,
issuer : "test-issuer" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
2020-11-30 23:02:03 +00:00
cache := & mockSessionCache { t : t , getReturnsToken : & oidctypes . Token {
IDToken : & oidctypes . IDToken {
2020-10-21 18:05:19 +00:00
Token : "test-id-token" ,
Expiry : metav1 . NewTime ( time . Now ( ) ) , // less than Now() + minIDTokenValidity
} ,
} }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : "test-issuer" ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
return WithSessionCache ( cache ) ( h )
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"test-issuer\"" } ,
2021-04-16 17:46:59 +00:00
wantErr : ` could not perform OIDC discovery for "test-issuer": Get "test-issuer/.well-known/openid-configuration": unsupported protocol scheme "" ` ,
2020-10-21 18:05:19 +00:00
} ,
{
name : "session cache hit with valid token" ,
issuer : "test-issuer" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : "test-issuer" ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
return WithSessionCache ( cache ) ( h )
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" } ,
2020-10-21 18:05:19 +00:00
wantToken : & testToken ,
} ,
2020-10-06 22:27:36 +00:00
{
2021-06-21 19:19:12 +00:00
name : "discovery failure due to 500 error" ,
2020-10-06 22:27:36 +00:00
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error { return nil }
} ,
2021-04-16 17:46:59 +00:00
issuer : errorServer . URL ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + errorServer . URL + "\"" } ,
2021-04-16 17:46:59 +00:00
wantErr : fmt . Sprintf ( "could not perform OIDC discovery for %q: 500 Internal Server Error: some discovery error\n" , errorServer . URL ) ,
2020-10-06 22:27:36 +00:00
} ,
2021-06-21 19:19:12 +00:00
{
name : "discovery failure due to invalid response_modes_supported" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error { return nil }
} ,
issuer : brokenResponseModeServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + brokenResponseModeServer . URL + "\"" } ,
wantErr : fmt . Sprintf ( "could not decode response_modes_supported in OIDC discovery from %q: json: cannot unmarshal string into Go struct field .response_modes_supported of type []string" , brokenResponseModeServer . URL ) ,
} ,
2020-10-22 21:12:02 +00:00
{
name : "session cache hit with refreshable token" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
2021-10-13 19:31:20 +00:00
h . getProvider = func ( config * oauth2 . Config , provider * oidc . Provider , client * http . Client ) provider . UpstreamOIDCIdentityProviderI {
2020-11-30 23:14:57 +00:00
mock := mockUpstream ( t )
mock . EXPECT ( ) .
2022-01-13 02:05:10 +00:00
ValidateTokenAndMergeWithUserInfo ( gomock . Any ( ) , HasAccessToken ( testToken . AccessToken . Token ) , nonce . Nonce ( "" ) , true , false ) .
2020-12-04 21:33:36 +00:00
Return ( & testToken , nil )
2021-10-13 19:31:20 +00:00
mock . EXPECT ( ) .
PerformRefresh ( gomock . Any ( ) , testToken . RefreshToken . Token ) .
DoAndReturn ( func ( ctx context . Context , refreshToken string ) ( * oauth2 . Token , error ) {
// Call the real production code to perform a refresh.
return upstreamoidc . New ( config , provider , client ) . PerformRefresh ( ctx , refreshToken )
} )
2020-11-30 23:14:57 +00:00
return mock
}
2020-11-30 23:02:03 +00:00
cache := & mockSessionCache { t : t , getReturnsToken : & oidctypes . Token {
IDToken : & oidctypes . IDToken {
2020-10-22 21:12:02 +00:00
Token : "expired-test-id-token" ,
Expiry : metav1 . Now ( ) , // less than Now() + minIDTokenValidity
} ,
2020-11-30 23:02:03 +00:00
RefreshToken : & oidctypes . RefreshToken { Token : "test-refresh-token" } ,
2020-10-22 21:12:02 +00:00
} }
t . Cleanup ( func ( ) {
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
require . Len ( t , cache . sawPutTokens , 1 )
require . Equal ( t , testToken . IDToken . Token , cache . sawPutTokens [ 0 ] . IDToken . Token )
} )
h . cache = cache
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\"" } ,
2020-10-22 21:12:02 +00:00
wantToken : & testToken ,
} ,
{
name : "session cache hit but refresh returns invalid token" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
2021-10-13 19:31:20 +00:00
h . getProvider = func ( config * oauth2 . Config , provider * oidc . Provider , client * http . Client ) provider . UpstreamOIDCIdentityProviderI {
2020-11-30 23:14:57 +00:00
mock := mockUpstream ( t )
mock . EXPECT ( ) .
2022-01-13 02:05:10 +00:00
ValidateTokenAndMergeWithUserInfo ( gomock . Any ( ) , HasAccessToken ( testToken . AccessToken . Token ) , nonce . Nonce ( "" ) , true , false ) .
2020-12-04 21:33:36 +00:00
Return ( nil , fmt . Errorf ( "some validation error" ) )
2021-10-13 19:31:20 +00:00
mock . EXPECT ( ) .
PerformRefresh ( gomock . Any ( ) , "test-refresh-token-returning-invalid-id-token" ) .
DoAndReturn ( func ( ctx context . Context , refreshToken string ) ( * oauth2 . Token , error ) {
// Call the real production code to perform a refresh.
return upstreamoidc . New ( config , provider , client ) . PerformRefresh ( ctx , refreshToken )
} )
2020-11-30 23:14:57 +00:00
return mock
}
2020-11-30 23:02:03 +00:00
cache := & mockSessionCache { t : t , getReturnsToken : & oidctypes . Token {
IDToken : & oidctypes . IDToken {
2020-10-22 21:12:02 +00:00
Token : "expired-test-id-token" ,
Expiry : metav1 . Now ( ) , // less than Now() + minIDTokenValidity
} ,
2020-11-30 23:02:03 +00:00
RefreshToken : & oidctypes . RefreshToken { Token : "test-refresh-token-returning-invalid-id-token" } ,
2020-10-22 21:12:02 +00:00
} }
t . Cleanup ( func ( ) {
require . Empty ( t , cache . sawPutKeys )
require . Empty ( t , cache . sawPutTokens )
} )
h . cache = cache
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\"" } ,
2020-11-30 23:14:57 +00:00
wantErr : "some validation error" ,
2020-10-22 21:12:02 +00:00
} ,
{
name : "session cache hit but refresh fails" ,
issuer : successServer . URL ,
clientID : "not-the-test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
2020-11-30 23:02:03 +00:00
cache := & mockSessionCache { t : t , getReturnsToken : & oidctypes . Token {
IDToken : & oidctypes . IDToken {
2020-10-22 21:12:02 +00:00
Token : "expired-test-id-token" ,
Expiry : metav1 . Now ( ) , // less than Now() + minIDTokenValidity
} ,
2020-11-30 23:02:03 +00:00
RefreshToken : & oidctypes . RefreshToken { Token : "test-refresh-token" } ,
2020-10-22 21:12:02 +00:00
} }
t . Cleanup ( func ( ) {
require . Empty ( t , cache . sawPutKeys )
require . Empty ( t , cache . sawPutTokens )
} )
h . cache = cache
2021-07-08 19:32:44 +00:00
h . listen = func ( string , string ) ( net . Listener , error ) { return nil , fmt . Errorf ( "some listen error" ) }
h . isTTY = func ( int ) bool { return false }
2020-10-22 21:12:02 +00:00
return nil
}
} ,
2021-07-08 19:32:44 +00:00
wantLogs : [ ] string {
` "level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"=" ` + successServer . URL + ` " ` ,
` "level"=4 "msg"="Pinniped: Refreshing cached token." ` ,
2021-10-22 21:06:31 +00:00
` "level"=4 "msg"="Pinniped: Refresh failed." "error"="oauth2: cannot fetch token: 400 Bad Request\nResponse: expected client_id 'test-client-id'\n" ` ,
2021-07-08 19:32:44 +00:00
` "msg"="could not open callback listener" "error"="some listen error" ` ,
} ,
2020-10-22 21:12:02 +00:00
// Expect this to fall through to the authorization code flow, so it fails here.
2021-07-08 19:32:44 +00:00
wantErr : "login failed: must have either a localhost listener or stdin must be a TTY" ,
2020-10-22 21:12:02 +00:00
} ,
2020-10-06 22:27:36 +00:00
{
2021-07-08 19:32:44 +00:00
name : "listen failure and non-tty stdin" ,
2020-10-06 22:27:36 +00:00
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
2021-07-08 19:32:44 +00:00
h . listen = func ( net string , addr string ) ( net . Listener , error ) {
assert . Equal ( t , "tcp" , net )
assert . Equal ( t , "localhost:0" , addr )
return nil , fmt . Errorf ( "some listen error" )
}
h . isTTY = func ( fd int ) bool {
assert . Equal ( t , fd , syscall . Stdin )
return false
}
2020-10-06 22:27:36 +00:00
return nil
}
} ,
2021-07-08 19:32:44 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string {
` "level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"=" ` + successServer . URL + ` " ` ,
` "msg"="could not open callback listener" "error"="some listen error" ` ,
} ,
wantErr : "login failed: must have either a localhost listener or stdin must be a TTY" ,
2020-10-06 22:27:36 +00:00
} ,
{
2021-07-09 03:26:21 +00:00
name : "listening disabled and manual prompt fails" ,
2020-10-06 22:27:36 +00:00
opt : func ( t * testing . T ) Option {
2021-07-08 19:32:44 +00:00
return func ( h * handlerState ) error {
2021-07-09 03:26:21 +00:00
require . NoError ( t , WithSkipListen ( ) ( h ) )
2021-07-08 19:32:44 +00:00
h . isTTY = func ( fd int ) bool { return true }
h . openURL = func ( authorizeURL string ) error {
parsed , err := url . Parse ( authorizeURL )
require . NoError ( t , err )
require . Equal ( t , "http://127.0.0.1:0/callback" , parsed . Query ( ) . Get ( "redirect_uri" ) )
require . Equal ( t , "form_post" , parsed . Query ( ) . Get ( "response_mode" ) )
return fmt . Errorf ( "some browser open error" )
}
2021-07-29 22:49:16 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
2021-07-08 19:32:44 +00:00
return "" , fmt . Errorf ( "some prompt error" )
}
return nil
}
2020-10-06 22:27:36 +00:00
} ,
2021-07-08 19:32:44 +00:00
issuer : formPostSuccessServer . URL ,
wantLogs : [ ] string {
` "level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"=" ` + formPostSuccessServer . URL + ` " ` ,
` "msg"="could not open browser" "error"="some browser open error" ` ,
} ,
wantErr : "error handling callback: failed to prompt for manual authorization code: some prompt error" ,
} ,
{
name : "listen success and manual prompt succeeds" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . listen = func ( string , string ) ( net . Listener , error ) { return nil , fmt . Errorf ( "some listen error" ) }
h . isTTY = func ( fd int ) bool { return true }
h . openURL = func ( authorizeURL string ) error {
parsed , err := url . Parse ( authorizeURL )
require . NoError ( t , err )
require . Equal ( t , "http://127.0.0.1:0/callback" , parsed . Query ( ) . Get ( "redirect_uri" ) )
require . Equal ( t , "form_post" , parsed . Query ( ) . Get ( "response_mode" ) )
return nil
}
2021-07-29 22:49:16 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
2021-07-08 19:32:44 +00:00
return "" , fmt . Errorf ( "some prompt error" )
}
return nil
}
} ,
issuer : formPostSuccessServer . URL ,
wantLogs : [ ] string {
` "level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"=" ` + formPostSuccessServer . URL + ` " ` ,
` "msg"="could not open callback listener" "error"="some listen error" ` ,
} ,
wantErr : "error handling callback: failed to prompt for manual authorization code: some prompt error" ,
2020-10-06 22:27:36 +00:00
} ,
{
name : "timeout waiting for callback" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
ctx , cancel := context . WithCancel ( h . ctx )
h . ctx = ctx
h . openURL = func ( _ string ) error {
cancel ( )
return nil
}
return nil
}
} ,
2021-04-16 17:46:59 +00:00
issuer : successServer . URL ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2021-04-16 17:46:59 +00:00
wantErr : "timed out waiting for token callback: context canceled" ,
2020-10-06 22:27:36 +00:00
} ,
{
name : "callback returns error" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . openURL = func ( _ string ) error {
go func ( ) {
h . callbacks <- callbackResult { err : fmt . Errorf ( "some callback error" ) }
} ( )
return nil
}
return nil
}
} ,
2021-04-16 17:46:59 +00:00
issuer : successServer . URL ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2021-04-16 17:46:59 +00:00
wantErr : "error handling callback: some callback error" ,
2020-10-06 22:27:36 +00:00
} ,
{
name : "callback returns success" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . generateState = func ( ) ( state . State , error ) { return "test-state" , nil }
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "test-pkce" , nil }
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "test-nonce" , nil }
2020-10-21 18:05:19 +00:00
cache := & mockSessionCache { t : t , getReturnsToken : nil }
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
2020-11-30 23:02:03 +00:00
require . Equal ( t , [ ] * oidctypes . Token { & testToken } , cache . sawPutTokens )
2020-10-21 18:05:19 +00:00
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
2020-11-16 17:54:13 +00:00
require . NoError ( t , WithClient ( & http . Client { Timeout : 10 * time . Second } ) ( h ) )
2020-10-21 18:05:19 +00:00
2020-10-06 22:27:36 +00:00
h . openURL = func ( actualURL string ) error {
parsedActualURL , err := url . Parse ( actualURL )
require . NoError ( t , err )
actualParams := parsedActualURL . Query ( )
require . Contains ( t , actualParams . Get ( "redirect_uri" ) , "http://127.0.0.1:" )
actualParams . Del ( "redirect_uri" )
require . Equal ( t , url . Values {
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
"code_challenge" : [ ] string { "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g" } ,
"code_challenge_method" : [ ] string { "S256" } ,
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { "test-scope" } ,
"nonce" : [ ] string { "test-nonce" } ,
"state" : [ ] string { "test-state" } ,
"access_type" : [ ] string { "offline" } ,
"client_id" : [ ] string { "test-client-id" } ,
} , actualParams )
parsedActualURL . RawQuery = ""
require . Equal ( t , successServer . URL + "/authorize" , parsedActualURL . String ( ) )
go func ( ) {
h . callbacks <- callbackResult { token : & testToken }
} ( )
return nil
}
return nil
}
} ,
issuer : successServer . URL ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-10-06 22:27:36 +00:00
wantToken : & testToken ,
} ,
2021-06-21 19:19:12 +00:00
{
name : "callback returns success with request_mode=form_post" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . generateState = func ( ) ( state . State , error ) { return "test-state" , nil }
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "test-pkce" , nil }
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "test-nonce" , nil }
cache := & mockSessionCache { t : t , getReturnsToken : nil }
cacheKey := SessionCacheKey {
Issuer : formPostSuccessServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
require . Equal ( t , [ ] * oidctypes . Token { & testToken } , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithClient ( & http . Client { Timeout : 10 * time . Second } ) ( h ) )
h . openURL = func ( actualURL string ) error {
parsedActualURL , err := url . Parse ( actualURL )
require . NoError ( t , err )
actualParams := parsedActualURL . Query ( )
require . Contains ( t , actualParams . Get ( "redirect_uri" ) , "http://127.0.0.1:" )
actualParams . Del ( "redirect_uri" )
require . Equal ( t , url . Values {
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
"code_challenge" : [ ] string { "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g" } ,
"code_challenge_method" : [ ] string { "S256" } ,
"response_type" : [ ] string { "code" } ,
"response_mode" : [ ] string { "form_post" } ,
"scope" : [ ] string { "test-scope" } ,
"nonce" : [ ] string { "test-nonce" } ,
"state" : [ ] string { "test-state" } ,
"access_type" : [ ] string { "offline" } ,
"client_id" : [ ] string { "test-client-id" } ,
} , actualParams )
parsedActualURL . RawQuery = ""
require . Equal ( t , formPostSuccessServer . URL + "/authorize" , parsedActualURL . String ( ) )
go func ( ) {
h . callbacks <- callbackResult { token : & testToken }
} ( )
return nil
}
return nil
}
} ,
issuer : formPostSuccessServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + formPostSuccessServer . URL + "\"" } ,
wantToken : & testToken ,
} ,
2021-04-20 00:59:46 +00:00
{
name : "upstream name and type are included in authorize request if upstream name is provided" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . generateState = func ( ) ( state . State , error ) { return "test-state" , nil }
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "test-pkce" , nil }
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "test-nonce" , nil }
cache := & mockSessionCache { t : t , getReturnsToken : nil }
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
require . Equal ( t , [ ] * oidctypes . Token { & testToken } , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithClient ( & http . Client { Timeout : 10 * time . Second } ) ( h ) )
require . NoError ( t , WithUpstreamIdentityProvider ( "some-upstream-name" , "oidc" ) ( h ) )
h . openURL = func ( actualURL string ) error {
parsedActualURL , err := url . Parse ( actualURL )
require . NoError ( t , err )
actualParams := parsedActualURL . Query ( )
require . Contains ( t , actualParams . Get ( "redirect_uri" ) , "http://127.0.0.1:" )
actualParams . Del ( "redirect_uri" )
require . Equal ( t , url . Values {
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
"code_challenge" : [ ] string { "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g" } ,
"code_challenge_method" : [ ] string { "S256" } ,
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { "test-scope" } ,
"nonce" : [ ] string { "test-nonce" } ,
"state" : [ ] string { "test-state" } ,
"access_type" : [ ] string { "offline" } ,
"client_id" : [ ] string { "test-client-id" } ,
2021-04-27 19:43:09 +00:00
"pinniped_idp_name" : [ ] string { "some-upstream-name" } ,
"pinniped_idp_type" : [ ] string { "oidc" } ,
2021-04-20 00:59:46 +00:00
} , actualParams )
parsedActualURL . RawQuery = ""
require . Equal ( t , successServer . URL + "/authorize" , parsedActualURL . String ( ) )
go func ( ) {
h . callbacks <- callbackResult { token : & testToken }
} ( )
return nil
}
return nil
}
} ,
issuer : successServer . URL ,
2021-05-11 18:09:37 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2021-04-20 00:59:46 +00:00
wantToken : & testToken ,
} ,
{
name : "ldap login when prompting for username returns an error" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
_ = defaultLDAPTestOpts ( t , h , nil , nil )
2021-06-30 20:06:37 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
2021-04-20 00:59:46 +00:00
require . Equal ( t , "Username: " , promptLabel )
return "" , errors . New ( "some prompt error" )
}
return nil
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : "error prompting for username: some prompt error" ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when prompting for password returns an error" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
_ = defaultLDAPTestOpts ( t , h , nil , nil )
2021-07-29 22:49:16 +00:00
h . promptForSecret = func ( _ string ) ( string , error ) { return "" , errors . New ( "some prompt error" ) }
2021-04-20 00:59:46 +00:00
return nil
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : "error prompting for password: some prompt error" ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when there is a problem with parsing the authorize URL" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
_ = defaultLDAPTestOpts ( t , h , nil , nil )
require . NoError ( t , WithClient ( & http . Client {
Transport : roundtripper . Func ( func ( req * http . Request ) ( * http . Response , error ) {
switch req . URL . Scheme + "://" + req . URL . Host + req . URL . Path {
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/.well-known/openid-configuration" :
type providerJSON struct {
Issuer string ` json:"issuer" `
AuthURL string ` json:"authorization_endpoint" `
TokenURL string ` json:"token_endpoint" `
JWKSURL string ` json:"jwks_uri" `
}
jsonResponseBody , err := json . Marshal ( & providerJSON {
Issuer : successServer . URL ,
AuthURL : "%" , // this is not a legal URL!
TokenURL : successServer . URL + "/token" ,
JWKSURL : successServer . URL + "/keys" ,
} )
require . NoError ( t , err )
return & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "content-type" : [ ] string { "application/json" } } ,
Body : ioutil . NopCloser ( strings . NewReader ( string ( jsonResponseBody ) ) ) ,
} , nil
default :
require . FailNow ( t , fmt . Sprintf ( "saw unexpected http call from the CLI: %s" , req . URL . String ( ) ) )
return nil , nil
}
} ) ,
} ) ( h ) )
return nil
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : ` could not build authorize request: parse "%?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&pinniped_idp_name=some-upstream-name&pinniped_idp_type=ldap&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state": invalid URL escape "%" ` ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when there is an error calling the authorization endpoint" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
return defaultLDAPTestOpts ( t , h , nil , errors . New ( "some error fetching authorize endpoint" ) )
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2021-04-20 00:59:46 +00:00
wantErr : ` authorization response error: Get "http:// ` + successServer . Listener . Addr ( ) . String ( ) +
2021-04-27 19:43:09 +00:00
` /authorize?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&pinniped_idp_name=some-upstream-name&pinniped_idp_type=ldap&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state": some error fetching authorize endpoint ` ,
2021-04-20 00:59:46 +00:00
} ,
{
2021-12-14 20:55:35 +00:00
name : "ldap login when the OIDC provider authorization endpoint returns something other than a redirect" ,
2021-04-20 00:59:46 +00:00
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
return defaultLDAPTestOpts ( t , h , & http . Response { StatusCode : http . StatusBadGateway , Status : "502 Bad Gateway" } , nil )
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : ` error getting authorization: expected to be redirected, but response status was 502 Bad Gateway ` ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when the OIDC provider authorization endpoint redirect has an error and error description" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
return defaultLDAPTestOpts ( t , h , & http . Response {
StatusCode : http . StatusFound ,
Header : http . Header { "Location" : [ ] string {
2021-04-20 01:08:52 +00:00
"http://127.0.0.1:0/callback?error=access_denied&error_description=optional-error-description&state=test-state" ,
2021-04-20 00:59:46 +00:00
} } ,
} , nil )
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : ` login failed with code "access_denied": optional-error-description ` ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when the OIDC provider authorization endpoint redirects us to a different server" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
return defaultLDAPTestOpts ( t , h , & http . Response {
StatusCode : http . StatusFound ,
Header : http . Header { "Location" : [ ] string {
"http://other-server.example.com/callback?code=foo&state=test-state" ,
} } ,
} , nil )
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : ` error getting authorization: redirected to the wrong location: http://other-server.example.com/callback?code=foo&state=test-state ` ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when the OIDC provider authorization endpoint redirect has an error but no error description" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
return defaultLDAPTestOpts ( t , h , & http . Response {
StatusCode : http . StatusFound ,
Header : http . Header { "Location" : [ ] string {
2021-04-20 01:08:52 +00:00
"http://127.0.0.1:0/callback?error=access_denied&state=test-state" ,
2021-04-20 00:59:46 +00:00
} } ,
} , nil )
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : ` login failed with code "access_denied" ` ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when the OIDC provider authorization endpoint redirect has the wrong state value" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
return defaultLDAPTestOpts ( t , h , & http . Response {
StatusCode : http . StatusFound ,
Header : http . Header { "Location" : [ ] string { "http://127.0.0.1:0/callback?code=foo&state=wrong-state" } } ,
} , nil )
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : ` missing or invalid state parameter in authorization response: http://127.0.0.1:0/callback?code=foo&state=wrong-state ` ,
2021-04-20 00:59:46 +00:00
} ,
{
name : "ldap login when there is an error exchanging the authcode or validating the tokens" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
fakeAuthCode := "test-authcode-value"
_ = defaultLDAPTestOpts ( t , h , & http . Response {
StatusCode : http . StatusFound ,
Header : http . Header { "Location" : [ ] string {
fmt . Sprintf ( "http://127.0.0.1:0/callback?code=%s&state=test-state" , fakeAuthCode ) ,
} } ,
} , nil )
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
mock := mockUpstream ( t )
mock . EXPECT ( ) .
ExchangeAuthcodeAndValidateTokens (
gomock . Any ( ) , fakeAuthCode , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , "http://127.0.0.1:0/callback" ) .
Return ( nil , errors . New ( "some authcode exchange or token validation error" ) )
return mock
}
return nil
}
} ,
2021-05-11 18:09:37 +00:00
issuer : successServer . URL ,
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
wantErr : "error during authorization code exchange: some authcode exchange or token validation error" ,
2021-04-20 00:59:46 +00:00
} ,
{
2021-07-19 23:20:59 +00:00
name : "successful ldap login with prompts for username and password" ,
2021-04-20 00:59:46 +00:00
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
fakeAuthCode := "test-authcode-value"
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
mock := mockUpstream ( t )
mock . EXPECT ( ) .
ExchangeAuthcodeAndValidateTokens (
gomock . Any ( ) , fakeAuthCode , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , "http://127.0.0.1:0/callback" ) .
Return ( & testToken , nil )
return mock
}
h . generateState = func ( ) ( state . State , error ) { return "test-state" , nil }
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "test-pkce" , nil }
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "test-nonce" , nil }
2021-07-19 23:20:59 +00:00
h . getEnv = func ( _ string ) string {
return "" // asking for any env var returns empty as if it were unset
}
2021-06-30 20:06:37 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
2021-04-20 00:59:46 +00:00
require . Equal ( t , "Username: " , promptLabel )
return "some-upstream-username" , nil
}
2021-07-29 22:49:16 +00:00
h . promptForSecret = func ( promptLabel string ) ( string , error ) {
2021-04-20 00:59:46 +00:00
require . Equal ( t , "Password: " , promptLabel )
return "some-upstream-password" , nil
}
cache := & mockSessionCache { t : t , getReturnsToken : nil }
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
require . Equal ( t , [ ] * oidctypes . Token { & testToken } , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
2021-05-13 17:05:56 +00:00
require . NoError ( t , WithCLISendingCredentials ( ) ( h ) )
2021-04-20 00:59:46 +00:00
require . NoError ( t , WithUpstreamIdentityProvider ( "some-upstream-name" , "ldap" ) ( h ) )
discoveryRequestWasMade := false
authorizeRequestWasMade := false
t . Cleanup ( func ( ) {
require . True ( t , discoveryRequestWasMade , "should have made an discovery request" )
require . True ( t , authorizeRequestWasMade , "should have made an authorize request" )
} )
require . NoError ( t , WithClient ( & http . Client {
Transport : roundtripper . Func ( func ( req * http . Request ) ( * http . Response , error ) {
switch req . URL . Scheme + "://" + req . URL . Host + req . URL . Path {
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/.well-known/openid-configuration" :
discoveryRequestWasMade = true
return defaultDiscoveryResponse ( req )
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/authorize" :
authorizeRequestWasMade = true
2021-05-12 20:06:08 +00:00
require . Equal ( t , "some-upstream-username" , req . Header . Get ( "Pinniped-Username" ) )
require . Equal ( t , "some-upstream-password" , req . Header . Get ( "Pinniped-Password" ) )
2021-04-20 00:59:46 +00:00
require . Equal ( t , url . Values {
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
"code_challenge" : [ ] string { "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g" } ,
"code_challenge_method" : [ ] string { "S256" } ,
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { "test-scope" } ,
"nonce" : [ ] string { "test-nonce" } ,
"state" : [ ] string { "test-state" } ,
"access_type" : [ ] string { "offline" } ,
"client_id" : [ ] string { "test-client-id" } ,
"redirect_uri" : [ ] string { "http://127.0.0.1:0/callback" } ,
2021-04-27 19:43:09 +00:00
"pinniped_idp_name" : [ ] string { "some-upstream-name" } ,
"pinniped_idp_type" : [ ] string { "ldap" } ,
2021-04-20 00:59:46 +00:00
} , req . URL . Query ( ) )
return & http . Response {
StatusCode : http . StatusFound ,
Header : http . Header { "Location" : [ ] string {
fmt . Sprintf ( "http://127.0.0.1:0/callback?code=%s&state=test-state" , fakeAuthCode ) ,
} } ,
} , nil
default :
// Note that "/token" requests should not be made. They are mocked by mocking calls to ExchangeAuthcodeAndValidateTokens().
require . FailNow ( t , fmt . Sprintf ( "saw unexpected http call from the CLI: %s" , req . URL . String ( ) ) )
return nil , nil
}
} ) ,
} ) ( h ) )
return nil
}
} ,
issuer : successServer . URL ,
2021-05-11 18:09:37 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2021-04-20 00:59:46 +00:00
wantToken : & testToken ,
} ,
2021-07-19 23:20:59 +00:00
{
name : "successful ldap login with env vars for username and password" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
fakeAuthCode := "test-authcode-value"
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
mock := mockUpstream ( t )
mock . EXPECT ( ) .
ExchangeAuthcodeAndValidateTokens (
gomock . Any ( ) , fakeAuthCode , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , "http://127.0.0.1:0/callback" ) .
Return ( & testToken , nil )
return mock
}
h . generateState = func ( ) ( state . State , error ) { return "test-state" , nil }
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "test-pkce" , nil }
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "test-nonce" , nil }
h . getEnv = func ( key string ) string {
switch key {
case "PINNIPED_USERNAME" :
return "some-upstream-username"
case "PINNIPED_PASSWORD" :
return "some-upstream-password"
default :
return "" // all other env vars are treated as if they are unset
}
}
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
require . FailNow ( t , fmt . Sprintf ( "saw unexpected prompt from the CLI: %q" , promptLabel ) )
return "" , nil
}
2021-07-29 22:49:16 +00:00
h . promptForSecret = func ( promptLabel string ) ( string , error ) {
2021-07-19 23:20:59 +00:00
require . FailNow ( t , fmt . Sprintf ( "saw unexpected prompt from the CLI: %q" , promptLabel ) )
return "" , nil
}
cache := & mockSessionCache { t : t , getReturnsToken : nil }
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
require . Equal ( t , [ ] * oidctypes . Token { & testToken } , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithCLISendingCredentials ( ) ( h ) )
require . NoError ( t , WithUpstreamIdentityProvider ( "some-upstream-name" , "ldap" ) ( h ) )
discoveryRequestWasMade := false
authorizeRequestWasMade := false
t . Cleanup ( func ( ) {
require . True ( t , discoveryRequestWasMade , "should have made an discovery request" )
require . True ( t , authorizeRequestWasMade , "should have made an authorize request" )
} )
require . NoError ( t , WithClient ( & http . Client {
Transport : roundtripper . Func ( func ( req * http . Request ) ( * http . Response , error ) {
switch req . URL . Scheme + "://" + req . URL . Host + req . URL . Path {
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/.well-known/openid-configuration" :
discoveryRequestWasMade = true
return defaultDiscoveryResponse ( req )
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/authorize" :
authorizeRequestWasMade = true
require . Equal ( t , "some-upstream-username" , req . Header . Get ( "Pinniped-Username" ) )
require . Equal ( t , "some-upstream-password" , req . Header . Get ( "Pinniped-Password" ) )
require . Equal ( t , url . Values {
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
"code_challenge" : [ ] string { "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g" } ,
"code_challenge_method" : [ ] string { "S256" } ,
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { "test-scope" } ,
"nonce" : [ ] string { "test-nonce" } ,
"state" : [ ] string { "test-state" } ,
"access_type" : [ ] string { "offline" } ,
"client_id" : [ ] string { "test-client-id" } ,
"redirect_uri" : [ ] string { "http://127.0.0.1:0/callback" } ,
"pinniped_idp_name" : [ ] string { "some-upstream-name" } ,
"pinniped_idp_type" : [ ] string { "ldap" } ,
} , req . URL . Query ( ) )
return & http . Response {
StatusCode : http . StatusFound ,
Header : http . Header { "Location" : [ ] string {
fmt . Sprintf ( "http://127.0.0.1:0/callback?code=%s&state=test-state" , fakeAuthCode ) ,
} } ,
} , nil
default :
// Note that "/token" requests should not be made. They are mocked by mocking calls to ExchangeAuthcodeAndValidateTokens().
require . FailNow ( t , fmt . Sprintf ( "saw unexpected http call from the CLI: %s" , req . URL . String ( ) ) )
return nil , nil
}
} ) ,
} ) ( h ) )
return nil
}
} ,
issuer : successServer . URL ,
wantLogs : [ ] string {
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Read username from environment variable\" \"name\"=\"PINNIPED_USERNAME\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Read password from environment variable\" \"name\"=\"PINNIPED_PASSWORD\"" ,
} ,
2021-12-14 20:55:35 +00:00
wantToken : & testToken ,
} ,
{
name : "successful ldap login with env vars for username and password, http.StatusSeeOther redirect" ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
fakeAuthCode := "test-authcode-value"
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
mock := mockUpstream ( t )
mock . EXPECT ( ) .
ExchangeAuthcodeAndValidateTokens (
gomock . Any ( ) , fakeAuthCode , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , "http://127.0.0.1:0/callback" ) .
Return ( & testToken , nil )
return mock
}
h . generateState = func ( ) ( state . State , error ) { return "test-state" , nil }
h . generatePKCE = func ( ) ( pkce . Code , error ) { return "test-pkce" , nil }
h . generateNonce = func ( ) ( nonce . Nonce , error ) { return "test-nonce" , nil }
h . getEnv = func ( key string ) string {
switch key {
case "PINNIPED_USERNAME" :
return "some-upstream-username"
case "PINNIPED_PASSWORD" :
return "some-upstream-password"
default :
return "" // all other env vars are treated as if they are unset
}
}
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
require . FailNow ( t , fmt . Sprintf ( "saw unexpected prompt from the CLI: %q" , promptLabel ) )
return "" , nil
}
h . promptForSecret = func ( promptLabel string ) ( string , error ) {
require . FailNow ( t , fmt . Sprintf ( "saw unexpected prompt from the CLI: %q" , promptLabel ) )
return "" , nil
}
cache := & mockSessionCache { t : t , getReturnsToken : nil }
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
require . Equal ( t , [ ] * oidctypes . Token { & testToken } , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithCLISendingCredentials ( ) ( h ) )
require . NoError ( t , WithUpstreamIdentityProvider ( "some-upstream-name" , "ldap" ) ( h ) )
discoveryRequestWasMade := false
authorizeRequestWasMade := false
t . Cleanup ( func ( ) {
require . True ( t , discoveryRequestWasMade , "should have made an discovery request" )
require . True ( t , authorizeRequestWasMade , "should have made an authorize request" )
} )
require . NoError ( t , WithClient ( & http . Client {
Transport : roundtripper . Func ( func ( req * http . Request ) ( * http . Response , error ) {
switch req . URL . Scheme + "://" + req . URL . Host + req . URL . Path {
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/.well-known/openid-configuration" :
discoveryRequestWasMade = true
return defaultDiscoveryResponse ( req )
case "http://" + successServer . Listener . Addr ( ) . String ( ) + "/authorize" :
authorizeRequestWasMade = true
require . Equal ( t , "some-upstream-username" , req . Header . Get ( "Pinniped-Username" ) )
require . Equal ( t , "some-upstream-password" , req . Header . Get ( "Pinniped-Password" ) )
require . Equal ( t , url . Values {
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
"code_challenge" : [ ] string { "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g" } ,
"code_challenge_method" : [ ] string { "S256" } ,
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { "test-scope" } ,
"nonce" : [ ] string { "test-nonce" } ,
"state" : [ ] string { "test-state" } ,
"access_type" : [ ] string { "offline" } ,
"client_id" : [ ] string { "test-client-id" } ,
"redirect_uri" : [ ] string { "http://127.0.0.1:0/callback" } ,
"pinniped_idp_name" : [ ] string { "some-upstream-name" } ,
"pinniped_idp_type" : [ ] string { "ldap" } ,
} , req . URL . Query ( ) )
return & http . Response {
StatusCode : http . StatusSeeOther ,
Header : http . Header { "Location" : [ ] string {
fmt . Sprintf ( "http://127.0.0.1:0/callback?code=%s&state=test-state" , fakeAuthCode ) ,
} } ,
} , nil
default :
// Note that "/token" requests should not be made. They are mocked by mocking calls to ExchangeAuthcodeAndValidateTokens().
require . FailNow ( t , fmt . Sprintf ( "saw unexpected http call from the CLI: %s" , req . URL . String ( ) ) )
return nil , nil
}
} ) ,
} ) ( h ) )
return nil
}
} ,
issuer : successServer . URL ,
wantLogs : [ ] string {
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Read username from environment variable\" \"name\"=\"PINNIPED_USERNAME\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Read password from environment variable\" \"name\"=\"PINNIPED_PASSWORD\"" ,
} ,
2021-07-19 23:20:59 +00:00
wantToken : & testToken ,
} ,
2020-12-04 23:33:53 +00:00
{
name : "with requested audience, session cache hit with valid token, but discovery fails" ,
clientID : "test-client-id" ,
issuer : errorServer . URL ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : errorServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "cluster-1234" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"cluster-1234\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + errorServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : fmt . Sprintf ( "failed to exchange token: could not perform OIDC discovery for %q: 500 Internal Server Error: some discovery error\n" , errorServer . URL ) ,
} ,
{
name : "with requested audience, session cache hit with valid token, but token URL is invalid" ,
issuer : brokenTokenURLServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : brokenTokenURLServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "cluster-1234" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"cluster-1234\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + brokenTokenURLServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : ` failed to exchange token: could not build RFC8693 request: parse "%": invalid URL escape "%" ` ,
} ,
{
name : "with requested audience, session cache hit with valid token, but token exchange request fails" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-invalid-http-response" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-http-response\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : fmt . Sprintf ( ` failed to exchange token: Post "%s/token": failed to parse Location header "%%": parse "%%": invalid URL escape "%%" ` , successServer . URL ) ,
} ,
{
name : "with requested audience, session cache hit with valid token, but token exchange request returns non-200" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-http-400" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-http-400\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : ` failed to exchange token: unexpected HTTP response status 400 ` ,
} ,
2020-12-10 16:09:42 +00:00
{
name : "with requested audience, session cache hit with valid token, but token exchange request returns invalid content-type header" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-invalid-content-type" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-content-type\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-10 16:09:42 +00:00
wantErr : ` failed to exchange token: failed to decode content-type header: mime: invalid media parameter ` ,
} ,
2020-12-04 23:33:53 +00:00
{
name : "with requested audience, session cache hit with valid token, but token exchange request returns wrong content-type" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-wrong-content-type" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-wrong-content-type\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : ` failed to exchange token: unexpected HTTP response content type "invalid" ` ,
} ,
{
name : "with requested audience, session cache hit with valid token, but token exchange request returns invalid JSON" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-invalid-json" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-json\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : ` failed to exchange token: failed to decode response: unexpected EOF ` ,
} ,
{
name : "with requested audience, session cache hit with valid token, but token exchange request returns invalid token_type" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-invalid-tokentype" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-tokentype\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : ` failed to exchange token: got unexpected token_type "invalid" ` ,
} ,
{
name : "with requested audience, session cache hit with valid token, but token exchange request returns invalid issued_token_type" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-invalid-issuedtokentype" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-issuedtokentype\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : ` failed to exchange token: got unexpected issued_token_type "invalid" ` ,
} ,
{
name : "with requested audience, session cache hit with valid token, but token exchange request returns invalid JWT" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience-produce-invalid-jwt" ) ( h ) )
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-jwt\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantErr : ` failed to exchange token: received invalid JWT: oidc: malformed jwt: square/go-jose: compact JWS format must have three parts ` ,
} ,
{
name : "with requested audience, session cache hit with valid token, and token exchange request succeeds" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & testToken }
t . Cleanup ( func ( ) {
require . Equal ( t , [ ] SessionCacheKey { {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
} } , cache . sawGetKeys )
require . Empty ( t , cache . sawPutTokens )
} )
require . NoError ( t , WithSessionCache ( cache ) ( h ) )
require . NoError ( t , WithRequestAudience ( "test-audience" ) ( h ) )
h . validateIDToken = func ( ctx context . Context , provider * oidc . Provider , audience string , token string ) ( * oidc . IDToken , error ) {
require . Equal ( t , "test-audience" , audience )
require . Equal ( t , "test-id-token-with-requested-audience" , token )
return & oidc . IDToken { Expiry : testExchangedToken . IDToken . Expiry . Time } , nil
}
return nil
}
} ,
2021-04-30 19:10:04 +00:00
wantLogs : [ ] string { "\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" } ,
2020-12-04 23:33:53 +00:00
wantToken : & testExchangedToken ,
} ,
{
name : "with requested audience, session cache hit with valid refresh token, and token exchange request succeeds" ,
issuer : successServer . URL ,
clientID : "test-client-id" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
cache := & mockSessionCache { t : t , getReturnsToken : & oidctypes . Token {
IDToken : & oidctypes . IDToken {
Token : "expired-test-id-token" ,
Expiry : metav1 . Now ( ) , // less than Now() + minIDTokenValidity
} ,
RefreshToken : & oidctypes . RefreshToken { Token : "test-refresh-token" } ,
} }
t . Cleanup ( func ( ) {
cacheKey := SessionCacheKey {
Issuer : successServer . URL ,
ClientID : "test-client-id" ,
Scopes : [ ] string { "test-scope" } ,
RedirectURI : "http://localhost:0/callback" ,
}
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawGetKeys )
require . Equal ( t , [ ] SessionCacheKey { cacheKey } , cache . sawPutKeys )
require . Len ( t , cache . sawPutTokens , 1 )
require . Equal ( t , testToken . IDToken . Token , cache . sawPutTokens [ 0 ] . IDToken . Token )
} )
h . cache = cache
2021-10-13 19:31:20 +00:00
h . getProvider = func ( config * oauth2 . Config , provider * oidc . Provider , client * http . Client ) provider . UpstreamOIDCIdentityProviderI {
2020-12-04 23:33:53 +00:00
mock := mockUpstream ( t )
mock . EXPECT ( ) .
2022-01-13 02:05:10 +00:00
ValidateTokenAndMergeWithUserInfo ( gomock . Any ( ) , HasAccessToken ( testToken . AccessToken . Token ) , nonce . Nonce ( "" ) , true , false ) .
2020-12-04 23:33:53 +00:00
Return ( & testToken , nil )
2021-10-13 19:31:20 +00:00
mock . EXPECT ( ) .
PerformRefresh ( gomock . Any ( ) , testToken . RefreshToken . Token ) .
DoAndReturn ( func ( ctx context . Context , refreshToken string ) ( * oauth2 . Token , error ) {
// Call the real production code to perform a refresh.
return upstreamoidc . New ( config , provider , client ) . PerformRefresh ( ctx , refreshToken )
} )
2020-12-04 23:33:53 +00:00
return mock
}
require . NoError ( t , WithRequestAudience ( "test-audience" ) ( h ) )
h . validateIDToken = func ( ctx context . Context , provider * oidc . Provider , audience string , token string ) ( * oidc . IDToken , error ) {
require . Equal ( t , "test-audience" , audience )
require . Equal ( t , "test-id-token-with-requested-audience" , token )
return & oidc . IDToken { Expiry : testExchangedToken . IDToken . Expiry . Time } , nil
}
return nil
}
} ,
2021-04-16 17:46:59 +00:00
wantLogs : [ ] string {
2021-04-30 19:10:04 +00:00
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer . URL + "\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\"" ,
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience\"" ,
2021-04-16 17:46:59 +00:00
} ,
2020-12-04 23:33:53 +00:00
wantToken : & testExchangedToken ,
} ,
2020-10-06 22:27:36 +00:00
}
for _ , tt := range tests {
tt := tt
t . Run ( tt . name , func ( t * testing . T ) {
2021-12-10 22:22:36 +00:00
testLogger := testlogger . NewLegacy ( t ) //nolint: staticcheck // old test with lots of log statements
klog . SetLogger ( testLogger . Logger )
2021-04-16 17:46:59 +00:00
2020-10-21 18:04:46 +00:00
tok , err := Login ( tt . issuer , tt . clientID ,
2020-10-06 22:27:36 +00:00
WithContext ( context . Background ( ) ) ,
WithListenPort ( 0 ) ,
WithScopes ( [ ] string { "test-scope" } ) ,
2021-07-08 19:32:44 +00:00
WithSkipBrowserOpen ( ) ,
2020-10-06 22:27:36 +00:00
tt . opt ( t ) ,
2021-12-10 22:22:36 +00:00
WithLogger ( testLogger . Logger ) ,
2020-10-06 22:27:36 +00:00
)
2021-07-08 19:32:44 +00:00
testLogger . Expect ( tt . wantLogs )
2020-10-06 22:27:36 +00:00
if tt . wantErr != "" {
require . EqualError ( t , err , tt . wantErr )
require . Nil ( t , tok )
return
}
2020-10-22 21:12:02 +00:00
require . NoError ( t , err )
if tt . wantToken == nil {
require . Nil ( t , tok )
return
}
require . NotNil ( t , tok )
if want := tt . wantToken . AccessToken ; want != nil {
require . NotNil ( t , tok . AccessToken )
require . Equal ( t , want . Token , tok . AccessToken . Token )
require . Equal ( t , want . Type , tok . AccessToken . Type )
2020-11-20 01:57:07 +00:00
testutil . RequireTimeInDelta ( t , want . Expiry . Time , tok . AccessToken . Expiry . Time , 5 * time . Second )
2020-10-22 21:12:02 +00:00
} else {
assert . Nil ( t , tok . AccessToken )
}
require . Equal ( t , tt . wantToken . RefreshToken , tok . RefreshToken )
if want := tt . wantToken . IDToken ; want != nil {
require . NotNil ( t , tok . IDToken )
require . Equal ( t , want . Token , tok . IDToken . Token )
2020-11-20 01:57:07 +00:00
testutil . RequireTimeInDelta ( t , want . Expiry . Time , tok . IDToken . Expiry . Time , 5 * time . Second )
2020-10-22 21:12:02 +00:00
} else {
assert . Nil ( t , tok . IDToken )
}
2020-10-06 22:27:36 +00:00
} )
}
}
2021-07-08 19:32:44 +00:00
func TestHandlePasteCallback ( t * testing . T ) {
const testRedirectURI = "http://127.0.0.1:12324/callback"
tests := [ ] struct {
name string
opt func ( t * testing . T ) Option
wantCallback * callbackResult
} {
{
name : "no stdin available" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . isTTY = func ( fd int ) bool {
require . Equal ( t , syscall . Stdin , fd )
return false
}
h . useFormPost = true
return nil
}
} ,
} ,
{
name : "no form_post mode available" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . isTTY = func ( fd int ) bool { return true }
h . useFormPost = false
return nil
}
} ,
} ,
{
name : "prompt fails" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . isTTY = func ( fd int ) bool { return true }
h . useFormPost = true
2021-07-29 22:49:16 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
assert . Equal ( t , " Optionally, paste your authorization code: " , promptLabel )
2021-07-08 19:32:44 +00:00
return "" , fmt . Errorf ( "some prompt error" )
}
return nil
}
} ,
wantCallback : & callbackResult {
err : fmt . Errorf ( "failed to prompt for manual authorization code: some prompt error" ) ,
} ,
} ,
{
name : "redeeming code fails" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . isTTY = func ( fd int ) bool { return true }
h . useFormPost = true
2021-07-29 22:49:16 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
2021-07-08 19:32:44 +00:00
return "invalid" , nil
}
h . oauth2Config = & oauth2 . Config { RedirectURL : testRedirectURI }
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
mock := mockUpstream ( t )
mock . EXPECT ( ) .
ExchangeAuthcodeAndValidateTokens ( gomock . Any ( ) , "invalid" , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , testRedirectURI ) .
Return ( nil , fmt . Errorf ( "some exchange error" ) )
return mock
}
return nil
}
} ,
wantCallback : & callbackResult {
err : fmt . Errorf ( "some exchange error" ) ,
} ,
} ,
{
name : "success" ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . isTTY = func ( fd int ) bool { return true }
h . useFormPost = true
2021-07-29 22:49:16 +00:00
h . promptForValue = func ( _ context . Context , promptLabel string ) ( string , error ) {
2021-07-08 19:32:44 +00:00
return "valid" , nil
}
h . oauth2Config = & oauth2 . Config { RedirectURL : testRedirectURI }
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
mock := mockUpstream ( t )
mock . EXPECT ( ) .
ExchangeAuthcodeAndValidateTokens ( gomock . Any ( ) , "valid" , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , testRedirectURI ) .
Return ( & oidctypes . Token { IDToken : & oidctypes . IDToken { Token : "test-id-token" } } , nil )
return mock
}
return nil
}
} ,
wantCallback : & callbackResult {
token : & oidctypes . Token { IDToken : & oidctypes . IDToken { Token : "test-id-token" } } ,
} ,
} ,
}
for _ , tt := range tests {
tt := tt
t . Run ( tt . name , func ( t * testing . T ) {
2022-02-05 00:57:37 +00:00
t . Parallel ( )
2021-07-08 19:32:44 +00:00
h := & handlerState {
callbacks : make ( chan callbackResult , 1 ) ,
state : state . State ( "test-state" ) ,
pkce : pkce . Code ( "test-pkce" ) ,
nonce : nonce . Nonce ( "test-nonce" ) ,
}
if tt . opt != nil {
require . NoError ( t , tt . opt ( t ) ( h ) )
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer cancel ( )
var buf bytes . Buffer
h . promptForWebLogin ( ctx , "https://test-authorize-url/" , & buf )
require . Equal ( t ,
"Log in by visiting this link:\n\n https://test-authorize-url/\n\n" ,
buf . String ( ) ,
)
if tt . wantCallback != nil {
select {
case <- time . After ( 1 * time . Second ) :
require . Fail ( t , "timed out waiting to receive from callbacks channel" )
case result := <- h . callbacks :
require . Equal ( t , * tt . wantCallback , result )
}
}
} )
}
}
2020-10-06 22:27:36 +00:00
func TestHandleAuthCodeCallback ( t * testing . T ) {
2020-12-02 16:36:07 +00:00
const testRedirectURI = "http://127.0.0.1:12324/callback"
2021-06-21 19:40:08 +00:00
withFormPostMode := func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . useFormPost = true
return nil
}
}
2020-10-06 22:27:36 +00:00
tests := [ ] struct {
2022-02-05 00:57:37 +00:00
name string
method string
query string
body [ ] byte
headers http . Header
opt func ( t * testing . T ) Option
wantErr string
wantHTTPStatus int
wantNoCallbacks bool
wantHeaders http . Header
2020-10-06 22:27:36 +00:00
} {
{
name : "wrong method" ,
2022-02-05 00:57:37 +00:00
method : http . MethodPost ,
2020-10-06 22:27:36 +00:00
query : "" ,
2022-02-05 00:57:37 +00:00
wantErr : "wanted GET but got POST" ,
2020-10-06 22:27:36 +00:00
wantHTTPStatus : http . StatusMethodNotAllowed ,
} ,
2021-06-21 19:40:08 +00:00
{
name : "wrong method for form_post" ,
2022-02-05 00:57:37 +00:00
method : http . MethodGet ,
2021-06-21 19:40:08 +00:00
query : "" ,
opt : withFormPostMode ,
2022-02-05 00:57:37 +00:00
wantErr : "wanted POST but got GET" ,
2021-06-21 19:40:08 +00:00
wantHTTPStatus : http . StatusMethodNotAllowed ,
} ,
{
name : "invalid form for form_post" ,
2022-02-05 00:57:37 +00:00
method : http . MethodPost ,
2021-06-21 19:40:08 +00:00
query : "" ,
2022-02-05 00:57:37 +00:00
headers : map [ string ] [ ] string { "Content-Type" : { "application/x-www-form-urlencoded" } } ,
2021-06-21 19:40:08 +00:00
body : [ ] byte ( ` % ` ) ,
opt : withFormPostMode ,
wantErr : ` invalid form: invalid URL escape "%" ` ,
wantHTTPStatus : http . StatusBadRequest ,
} ,
2020-10-06 22:27:36 +00:00
{
name : "invalid state" ,
query : "state=invalid" ,
wantErr : "missing or invalid state parameter" ,
wantHTTPStatus : http . StatusForbidden ,
} ,
{
name : "error code from provider" ,
query : "state=test-state&error=some_error" ,
wantErr : ` login failed with code "some_error" ` ,
wantHTTPStatus : http . StatusBadRequest ,
} ,
2021-04-20 01:08:52 +00:00
{
name : "error code with a description from provider" ,
query : "state=test-state&error=some_error&error_description=optional%20error%20description" ,
wantErr : ` login failed with code "some_error": optional error description ` ,
wantHTTPStatus : http . StatusBadRequest ,
} ,
2022-02-05 00:57:37 +00:00
{
name : "in form post mode, invalid issuer url config during CORS preflight request returns an error" ,
method : http . MethodOptions ,
query : "" ,
headers : map [ string ] [ ] string { "Origin" : { "https://some-origin.com" } } ,
wantErr : ` invalid issuer url: parse "://bad-url": missing protocol scheme ` ,
wantHTTPStatus : http . StatusInternalServerError ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . useFormPost = true
h . issuer = "://bad-url"
return nil
}
} ,
} ,
{
name : "in form post mode, options request is missing origin header results in 400 and keeps listener running" ,
method : http . MethodOptions ,
query : "" ,
opt : withFormPostMode ,
wantNoCallbacks : true ,
wantHTTPStatus : http . StatusBadRequest ,
} ,
{
name : "in form post mode, valid CORS request responds with 402 and CORS headers and keeps listener running" ,
method : http . MethodOptions ,
query : "" ,
headers : map [ string ] [ ] string { "Origin" : { "https://some-origin.com" } } ,
wantNoCallbacks : true ,
wantHTTPStatus : http . StatusNoContent ,
wantHeaders : map [ string ] [ ] string {
"Access-Control-Allow-Credentials" : { "false" } ,
"Access-Control-Allow-Methods" : { "POST, OPTIONS" } ,
"Access-Control-Allow-Origin" : { "https://valid-issuer.com" } ,
"Access-Control-Allow-Private-Network" : { "true" } ,
} ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . useFormPost = true
h . issuer = "https://valid-issuer.com/with/some/path"
return nil
}
} ,
} ,
{
name : "in form post mode, valid CORS request with Access-Control-Request-Headers responds with 402 and CORS headers including Access-Control-Allow-Headers and keeps listener running" ,
method : http . MethodOptions ,
query : "" ,
headers : map [ string ] [ ] string {
"Origin" : { "https://some-origin.com" } ,
"Access-Control-Request-Headers" : { "header1, header2, header3" } ,
} ,
wantNoCallbacks : true ,
wantHTTPStatus : http . StatusNoContent ,
wantHeaders : map [ string ] [ ] string {
"Access-Control-Allow-Credentials" : { "false" } ,
"Access-Control-Allow-Methods" : { "POST, OPTIONS" } ,
"Access-Control-Allow-Origin" : { "https://valid-issuer.com" } ,
"Access-Control-Allow-Private-Network" : { "true" } ,
"Access-Control-Allow-Headers" : { "header1, header2, header3" } ,
} ,
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . useFormPost = true
h . issuer = "https://valid-issuer.com/with/some/path"
return nil
}
} ,
} ,
2020-10-06 22:27:36 +00:00
{
name : "invalid code" ,
query : "state=test-state&code=invalid" ,
2020-11-30 23:14:57 +00:00
wantErr : "could not complete code exchange: some exchange error" ,
2020-10-06 22:27:36 +00:00
wantHTTPStatus : http . StatusBadRequest ,
2020-11-30 23:14:57 +00:00
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
2020-12-02 16:36:07 +00:00
h . oauth2Config = & oauth2 . Config { RedirectURL : testRedirectURI }
2020-12-02 16:27:20 +00:00
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
2020-11-30 23:14:57 +00:00
mock := mockUpstream ( t )
mock . EXPECT ( ) .
2020-12-02 16:36:07 +00:00
ExchangeAuthcodeAndValidateTokens ( gomock . Any ( ) , "invalid" , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , testRedirectURI ) .
2020-12-04 21:33:36 +00:00
Return ( nil , fmt . Errorf ( "some exchange error" ) )
2020-11-30 23:14:57 +00:00
return mock
}
return nil
}
} ,
2020-10-06 22:27:36 +00:00
} ,
{
2022-02-05 00:57:37 +00:00
name : "valid" ,
query : "state=test-state&code=valid" ,
wantHTTPStatus : http . StatusOK ,
2020-11-30 23:14:57 +00:00
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
2020-12-02 16:36:07 +00:00
h . oauth2Config = & oauth2 . Config { RedirectURL : testRedirectURI }
2020-12-02 16:27:20 +00:00
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
2020-11-30 23:14:57 +00:00
mock := mockUpstream ( t )
mock . EXPECT ( ) .
2020-12-02 16:36:07 +00:00
ExchangeAuthcodeAndValidateTokens ( gomock . Any ( ) , "valid" , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , testRedirectURI ) .
2020-12-04 21:33:36 +00:00
Return ( & oidctypes . Token { IDToken : & oidctypes . IDToken { Token : "test-id-token" } } , nil )
2020-11-30 23:14:57 +00:00
return mock
}
return nil
}
} ,
2020-10-06 22:27:36 +00:00
} ,
2021-06-21 19:40:08 +00:00
{
2022-02-05 00:57:37 +00:00
name : "valid form_post" ,
method : http . MethodPost ,
headers : map [ string ] [ ] string { "Content-Type" : { "application/x-www-form-urlencoded" } } ,
body : [ ] byte ( ` state=test-state&code=valid ` ) ,
wantHTTPStatus : http . StatusOK ,
2021-06-21 19:40:08 +00:00
opt : func ( t * testing . T ) Option {
return func ( h * handlerState ) error {
h . useFormPost = true
h . oauth2Config = & oauth2 . Config { RedirectURL : testRedirectURI }
h . getProvider = func ( _ * oauth2 . Config , _ * oidc . Provider , _ * http . Client ) provider . UpstreamOIDCIdentityProviderI {
mock := mockUpstream ( t )
mock . EXPECT ( ) .
ExchangeAuthcodeAndValidateTokens ( gomock . Any ( ) , "valid" , pkce . Code ( "test-pkce" ) , nonce . Nonce ( "test-nonce" ) , testRedirectURI ) .
Return ( & oidctypes . Token { IDToken : & oidctypes . IDToken { Token : "test-id-token" } } , nil )
return mock
}
return nil
}
} ,
} ,
2020-10-06 22:27:36 +00:00
}
for _ , tt := range tests {
tt := tt
t . Run ( tt . name , func ( t * testing . T ) {
2022-02-05 00:57:37 +00:00
t . Parallel ( )
2020-10-06 22:27:36 +00:00
h := & handlerState {
callbacks : make ( chan callbackResult , 1 ) ,
state : state . State ( "test-state" ) ,
pkce : pkce . Code ( "test-pkce" ) ,
nonce : nonce . Nonce ( "test-nonce" ) ,
2022-02-05 00:57:37 +00:00
logger : testlogger . New ( t ) . Logger ,
2020-11-30 23:14:57 +00:00
}
if tt . opt != nil {
require . NoError ( t , tt . opt ( t ) ( h ) )
2020-10-06 22:27:36 +00:00
}
2021-03-05 01:25:43 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Minute )
2020-10-06 22:27:36 +00:00
defer cancel ( )
resp := httptest . NewRecorder ( )
2021-06-21 19:40:08 +00:00
req , err := http . NewRequestWithContext ( ctx , "GET" , "/test-callback" , bytes . NewBuffer ( tt . body ) )
2020-10-06 22:27:36 +00:00
require . NoError ( t , err )
req . URL . RawQuery = tt . query
if tt . method != "" {
req . Method = tt . method
}
2022-02-05 00:57:37 +00:00
if tt . headers != nil {
req . Header = tt . headers
2021-06-21 19:40:08 +00:00
}
2020-10-06 22:27:36 +00:00
err = h . handleAuthCodeCallback ( resp , req )
if tt . wantErr != "" {
require . EqualError ( t , err , tt . wantErr )
if tt . wantHTTPStatus != 0 {
rec := httptest . NewRecorder ( )
err . ( httperr . Responder ) . Respond ( rec )
require . Equal ( t , tt . wantHTTPStatus , rec . Code )
}
} else {
require . NoError ( t , err )
2022-02-05 00:57:37 +00:00
require . Equal ( t , tt . wantHTTPStatus , resp . Code )
}
if tt . wantHeaders != nil {
require . Equal ( t , tt . wantHeaders , resp . Header ( ) )
2020-10-06 22:27:36 +00:00
}
select {
case <- time . After ( 1 * time . Second ) :
2022-02-05 00:57:37 +00:00
if ! tt . wantNoCallbacks {
require . Fail ( t , "timed out waiting to receive from callbacks channel" )
}
2020-10-06 22:27:36 +00:00
case result := <- h . callbacks :
if tt . wantErr != "" {
require . EqualError ( t , result . err , tt . wantErr )
return
}
require . NoError ( t , result . err )
require . NotNil ( t , result . token )
2020-11-30 23:14:57 +00:00
require . Equal ( t , result . token . IDToken . Token , "test-id-token" )
2020-10-06 22:27:36 +00:00
}
} )
}
}
2020-11-30 23:14:57 +00:00
func mockUpstream ( t * testing . T ) * mockupstreamoidcidentityprovider . MockUpstreamOIDCIdentityProviderI {
t . Helper ( )
ctrl := gomock . NewController ( t )
t . Cleanup ( ctrl . Finish )
return mockupstreamoidcidentityprovider . NewMockUpstreamOIDCIdentityProviderI ( ctrl )
}
2020-10-06 22:27:36 +00:00
2020-11-30 23:14:57 +00:00
// hasAccessTokenMatcher is a gomock.Matcher that expects an *oauth2.Token with a particular access token.
type hasAccessTokenMatcher struct { expected string }
func ( m hasAccessTokenMatcher ) Matches ( arg interface { } ) bool {
return arg . ( * oauth2 . Token ) . AccessToken == m . expected
2020-10-06 22:27:36 +00:00
}
2020-10-22 21:12:02 +00:00
2020-11-30 23:14:57 +00:00
func ( m hasAccessTokenMatcher ) Got ( got interface { } ) string {
return got . ( * oauth2 . Token ) . AccessToken
}
2020-10-22 21:12:02 +00:00
2020-11-30 23:14:57 +00:00
func ( m hasAccessTokenMatcher ) String ( ) string {
return m . expected
}
2020-10-22 21:12:02 +00:00
2020-11-30 23:14:57 +00:00
func HasAccessToken ( expected string ) gomock . Matcher {
return hasAccessTokenMatcher { expected : expected }
}