func TestServerRefreshToken(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} credXXX := oidc.ClientCredentials{ ID: "XXX", Secret: "secret", } credYYY := oidc.ClientCredentials{ ID: "YYY", Secret: "secret", } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} tests := []struct { token string clientID string // The client that associates with the token. creds oidc.ClientCredentials signer jose.Signer err error }{ // Everything is good. { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credXXX, signerFixture, nil, }, // Invalid refresh token(malformatted). { "invalid-token", "XXX", credXXX, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid payload content). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-2"))), "XXX", credXXX, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid ID content). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credXXX, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid client(client is not associated with the token). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credYYY, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no client ID). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no such client). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "AAA", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no secrets). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "XXX"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(invalid secret). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "XXX", Secret: "bad-secret"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Signing operation fails. { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credXXX, &StaticSigner{sig: nil, err: errors.New("fail")}, oauth2.NewError(oauth2.ErrorServerError), }, } for i, tt := range tests { km := &StaticKeyManager{ signer: tt.signer, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{Credentials: credXXX}, oidc.ClientIdentity{Credentials: credYYY}, }) userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientIdentityRepo: ciRepo, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } if _, err := refreshTokenRepo.Create("testid-1", tt.clientID); err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, err := srv.RefreshToken(tt.creds, tt.token) if !reflect.DeepEqual(err, tt.err) { t.Errorf("Case %d: expect: %v, got: %v", i, tt.err, err) } if jwt != nil { if string(jwt.Signature) != "beer" { t.Errorf("Case %d: expect signature: beer, got signature: %v", i, jwt.Signature) } claims, err := jwt.Claims() if err != nil { t.Errorf("Case %d: unexpected error: %v", i, err) } if claims["iss"] != issuerURL.String() || claims["sub"] != "testid-1" || claims["aud"] != "XXX" { t.Errorf("Case %d: invalid claims: %v", i, claims) } } } // Test that we should return error when user cannot be found after // verifying the token. km := &StaticKeyManager{ signer: signerFixture, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{Credentials: credXXX}, oidc.ClientIdentity{Credentials: credYYY}, }) userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } // Create a user that will be removed later. if err := userRepo.Create(nil, user.User{ ID: "testid-2", Email: "*****@*****.**", }); err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientIdentityRepo: ciRepo, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } if _, err := refreshTokenRepo.Create("testid-2", credXXX.ID); err != nil { t.Fatalf("Unexpected error: %v", err) } // Recreate the user repo to remove the user we created. userRepo, err = makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv.UserRepo = userRepo _, err = srv.RefreshToken(credXXX, fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1")))) if !reflect.DeepEqual(err, oauth2.NewError(oauth2.ErrorServerError)) { t.Errorf("Expect: %v, got: %v", oauth2.NewError(oauth2.ErrorServerError), err) } }
func TestServerCodeToken(t *testing.T) { ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientIdentityRepo: ciRepo, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } tests := []struct { scope []string refreshToken string }{ // No 'offline_access' in scope, should get empty refresh token. { scope: []string{"openid"}, refreshToken: "", }, // Have 'offline_access' in scope, should get non-empty refresh token. { scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, } for i, tt := range tests { sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false, tt.scope) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } _, err = sm.AttachUser(sessionID, "testid-1") if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } key, err := sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } jwt, token, err := srv.CodeToken(ci.Credentials, key) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } if jwt == nil { t.Fatalf("case %d: expect non-nil jwt", i) } if token != tt.refreshToken { t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token) } } }
func TestServerTokenFail(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} keyFixture := "goodkey" ccFixture := oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} tests := []struct { signer jose.Signer argCC oidc.ClientCredentials argKey string err error scope []string refreshToken string }{ // control test case to make sure fixtures check out { signer: signerFixture, argCC: ccFixture, argKey: keyFixture, scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, // no 'offline_access' in 'scope', should get empty refresh token { signer: signerFixture, argCC: ccFixture, argKey: keyFixture, scope: []string{"openid"}, }, // unrecognized key { signer: signerFixture, argCC: ccFixture, argKey: "foo", err: oauth2.NewError(oauth2.ErrorInvalidGrant), scope: []string{"openid", "offline_access"}, }, // unrecognized client { signer: signerFixture, argCC: oidc.ClientCredentials{ID: "YYY"}, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorInvalidClient), scope: []string{"openid", "offline_access"}, }, // signing operation fails { signer: &StaticSigner{sig: nil, err: errors.New("fail")}, argCC: ccFixture, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorServerError), scope: []string{"openid", "offline_access"}, }, } for i, tt := range tests { sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) sm.GenerateCode = func() (string, error) { return keyFixture, nil } sessionID, err := sm.NewSession("connector_id", ccFixture.ID, "bogus", url.URL{}, "", false, tt.scope) if err != nil { t.Fatalf("Unexpected error: %v", err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Errorf("case %d: unexpected error: %v", i, err) continue } km := &StaticKeyManager{ signer: tt.signer, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{Credentials: ccFixture}, }) _, err = sm.AttachUser(sessionID, "testid-1") if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: issuerURL, KeyManager: km, SessionManager: sm, ClientIdentityRepo: ciRepo, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } _, err = sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, token, err := srv.CodeToken(tt.argCC, tt.argKey) if token != tt.refreshToken { fmt.Printf("case %d: expect refresh token %q, got %q\n", i, tt.refreshToken, token) t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token) panic("") } if !reflect.DeepEqual(err, tt.err) { t.Errorf("case %d: expect %v, got %v", i, tt.err, err) } if err == nil && jwt == nil { t.Errorf("case %d: got nil JWT", i) } if err != nil && jwt != nil { t.Errorf("case %d: got non-nil JWT %v", i, jwt) } } }
func TestServerRefreshToken(t *testing.T) { clientB := client.Client{ Credentials: oidc.ClientCredentials{ ID: "example2.com", Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "example2.com", Path: "one/two/three"}, }, }, } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} // NOTE(ericchiang): These tests assume that the database ID of the first // refresh token will be "1". tests := []struct { token string expectedRefreshToken string clientID string // The client that associates with the token. creds oidc.ClientCredentials signer jose.Signer createScopes []string refreshScopes []string expectedAud []string err error }{ // Everything is good. { token: getRefreshTokenEncoded("1", "refresh-1"), expectedRefreshToken: getRefreshTokenEncoded("1", "refresh-2"), clientID: testClientID, creds: testClientCredentials, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, }, // Asking for a scope not originally granted to you. { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: testClientID, creds: testClientCredentials, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile", "extra_scope"}, err: oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(malformatted). { token: "invalid-token", clientID: testClientID, creds: testClientCredentials, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid payload content). { token: getRefreshTokenEncoded("1", "refresh-2"), clientID: testClientID, creds: testClientCredentials, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid ID content). { token: getRefreshTokenEncoded("0", "refresh-1"), clientID: testClientID, creds: testClientCredentials, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid client(client is not associated with the token). { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: testClientID, creds: clientB.Credentials, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no client ID). { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: testClientID, creds: oidc.ClientCredentials{ID: "", Secret: "aaa"}, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no such client). { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: testClientID, creds: oidc.ClientCredentials{ID: "AAA", Secret: "aaa"}, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no secrets). { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: testClientID, creds: oidc.ClientCredentials{ID: testClientID}, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(invalid secret). { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: testClientID, creds: oidc.ClientCredentials{ID: "bad-id", Secret: "bad-secret"}, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorInvalidClient), }, // Signing operation fails. { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: testClientID, creds: testClientCredentials, signer: &StaticSigner{sig: nil, err: errors.New("fail")}, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile"}, err: oauth2.NewError(oauth2.ErrorServerError), }, // Valid Cross-Client { token: getRefreshTokenEncoded("1", "refresh-1"), expectedRefreshToken: getRefreshTokenEncoded("1", "refresh-2"), clientID: "client_a", creds: oidc.ClientCredentials{ ID: "client_a", Secret: base64.URLEncoding.EncodeToString( []byte("client_a_secret")), }, signer: signerFixture, createScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b"}, refreshScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b"}, expectedAud: []string{"client_b"}, }, // Valid Cross-Client - but this time we leave out the scopes in the // refresh request, which should result in the original stored scopes // being used. { token: getRefreshTokenEncoded("1", "refresh-1"), expectedRefreshToken: getRefreshTokenEncoded("1", "refresh-2"), clientID: "client_a", creds: oidc.ClientCredentials{ ID: "client_a", Secret: base64.URLEncoding.EncodeToString( []byte("client_a_secret")), }, signer: signerFixture, createScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b"}, refreshScopes: []string{}, expectedAud: []string{"client_b"}, }, // Valid Cross-Client - asking for fewer scopes than originally used // when creating the refresh token, which is ok. { token: getRefreshTokenEncoded("1", "refresh-1"), expectedRefreshToken: getRefreshTokenEncoded("1", "refresh-2"), clientID: "client_a", creds: oidc.ClientCredentials{ ID: "client_a", Secret: base64.URLEncoding.EncodeToString( []byte("client_a_secret")), }, signer: signerFixture, createScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b", scope.ScopeGoogleCrossClient + "client_c"}, refreshScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b"}, expectedAud: []string{"client_b"}, }, // Valid Cross-Client - asking for multiple clients in the audience. { token: getRefreshTokenEncoded("1", "refresh-1"), expectedRefreshToken: getRefreshTokenEncoded("1", "refresh-2"), clientID: "client_a", creds: oidc.ClientCredentials{ ID: "client_a", Secret: base64.URLEncoding.EncodeToString( []byte("client_a_secret")), }, signer: signerFixture, createScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b", scope.ScopeGoogleCrossClient + "client_c"}, refreshScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b", scope.ScopeGoogleCrossClient + "client_c"}, expectedAud: []string{"client_b", "client_c"}, }, // Invalid Cross-Client - didn't orignally request cross-client when // refresh token was created. { token: getRefreshTokenEncoded("1", "refresh-1"), clientID: "client_a", creds: oidc.ClientCredentials{ ID: "client_a", Secret: base64.URLEncoding.EncodeToString( []byte("client_a_secret")), }, signer: signerFixture, createScopes: []string{"openid", "profile"}, refreshScopes: []string{"openid", "profile", scope.ScopeGoogleCrossClient + "client_b"}, err: oauth2.NewError(oauth2.ErrorInvalidRequest), }, } for i, tt := range tests { km := &StaticKeyManager{ signer: tt.signer, } f, err := makeCrossClientTestFixtures() if err != nil { t.Fatalf("error making test fixtures: %v", err) } f.srv.RefreshTokenRepo = refreshtest.NewTestRefreshTokenRepo() f.srv.KeyManager = km _, err = f.clientRepo.New(nil, clientB) if err != nil { t.Errorf("case %d: error creating other client: %v", i, err) } if _, err := f.srv.RefreshTokenRepo.Create(testUserID1, tt.clientID, "", tt.createScopes); err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, refreshToken, expiresIn, err := f.srv.RefreshToken(tt.creds, tt.refreshScopes, tt.token) if !reflect.DeepEqual(err, tt.err) { t.Errorf("Case %d: expect: %v, got: %v", i, tt.err, err) } if jwt != nil { if string(jwt.Signature) != "beer" { t.Errorf("Case %d: expect signature: beer, got signature: %v", i, jwt.Signature) } claims, err := jwt.Claims() if err != nil { t.Errorf("Case %d: unexpected error: %v", i, err) } var expectedAud interface{} if tt.expectedAud == nil { expectedAud = testClientID } else if len(tt.expectedAud) == 1 { expectedAud = tt.expectedAud[0] } else { expectedAud = tt.expectedAud } if claims["iss"] != testIssuerURL.String() { t.Errorf("Case %d: want=%v, got=%v", i, testIssuerURL.String(), claims["iss"]) } if claims["sub"] != testUserID1 { t.Errorf("Case %d: want=%v, got=%v", i, testUserID1, claims["sub"]) } if diff := pretty.Compare(claims["aud"], expectedAud); diff != "" { t.Errorf("Case %d: want=%v, got=%v", i, expectedAud, claims["aud"]) } } if diff := pretty.Compare(refreshToken, tt.expectedRefreshToken); diff != "" { t.Errorf("Case %d: want=%v, got=%v", i, tt.expectedRefreshToken, refreshToken) } if err == nil && expiresIn.IsZero() { t.Errorf("case %d: got zero expiration time %v", i, expiresIn) } } }
func TestHTTPExchangeTokenRefreshToken(t *testing.T) { password, err := user.NewPasswordFromPlaintext("woof") if err != nil { t.Fatalf("unexpectd error: %q", err) } passwordInfo := user.PasswordInfo{ UserID: "elroy77", Password: password, } cfg := &connector.LocalConnectorConfig{ ID: "local", } validRedirURL := url.URL{ Scheme: "http", Host: "client.example.com", Path: "/callback", } ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: validRedirURL.Host, Secret: base64.URLEncoding.EncodeToString([]byte("secret")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, } dbMap := db.NewMemDB() clientRepo, clientManager, err := makeClientRepoAndManager(dbMap, []client.LoadableClient{{ Client: ci, }}) if err != nil { t.Fatalf("Failed to create client identity manager: " + err.Error()) } passwordInfoRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(db.NewMemDB(), []user.PasswordInfo{passwordInfo}) if err != nil { t.Fatalf("Failed to create password info repo: %v", err) } issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} sm := manager.NewSessionManager(db.NewSessionRepo(dbMap), db.NewSessionKeyRepo(dbMap)) k, err := key.GeneratePrivateKey() if err != nil { t.Fatalf("Unable to generate RSA key: %v", err) } km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(time.Minute))) if err != nil { t.Fatalf("Unexpected error: %v", err) } usr := user.User{ ID: "ID-test", Email: "*****@*****.**", DisplayName: "displayname", } userRepo := db.NewUserRepo(db.NewMemDB()) if err := userRepo.Create(nil, usr); err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &server.Server{ IssuerURL: issuerURL, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, Templates: template.New(connector.LoginPageTemplateName), Connectors: []connector.Connector{}, UserRepo: userRepo, PasswordInfoRepo: passwordInfoRepo, RefreshTokenRepo: refreshTokenRepo, } if err = srv.AddConnector(cfg); err != nil { t.Fatalf("Unexpected error: %v", err) } sClient := &phttp.HandlerClient{Handler: srv.HTTPHandler()} pcfg, err := oidc.FetchProviderConfig(sClient, issuerURL.String()) if err != nil { t.Fatalf("Failed to fetch provider config: %v", err) } ks := key.NewPublicKeySet([]jose.JWK{k.JWK()}, time.Now().Add(1*time.Hour)) ccfg := oidc.ClientConfig{ HTTPClient: sClient, ProviderConfig: pcfg, Credentials: ci.Credentials, RedirectURL: validRedirURL.String(), KeySet: *ks, } cl, err := oidc.NewClient(ccfg) if err != nil { t.Fatalf("Failed creating oidc.Client: %v", err) } m := http.NewServeMux() var claims jose.Claims var refresh string m.HandleFunc("/callback", handleCallbackFunc(cl, &claims, &refresh)) cClient := &phttp.HandlerClient{Handler: m} // this will actually happen due to some interaction between the // end-user and a remote identity provider sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false, []string{"openid", "offline_access", "email", "profile"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } if _, err = sm.AttachRemoteIdentity(sessionID, passwordInfo.Identity()); err != nil { t.Fatalf("Unexpected error: %v", err) } if _, err = sm.AttachUser(sessionID, usr.ID); err != nil { t.Fatalf("Unexpected error: %v", err) } key, err := sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } req, err := http.NewRequest("GET", fmt.Sprintf("http://client.example.com/callback?code=%s", key), nil) if err != nil { t.Fatalf("Failed creating HTTP request: %v", err) } resp, err := cClient.Do(req) if err != nil { t.Fatalf("Failed resolving HTTP requests against /callback: %v", err) } if err := verifyUserClaims(claims, &ci, &usr, issuerURL); err != nil { t.Fatalf("Failed to verify claims: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Received status code %d, want %d", resp.StatusCode, http.StatusOK) } if refresh == "" { t.Fatalf("No refresh token") } // Use refresh token to get a new ID token. token, err := cl.RefreshToken(refresh) if err != nil { t.Fatalf("Unexpected error: %v", err) } claims, err = token.Claims() if err != nil { t.Fatalf("Failed parsing claims from client token: %v", err) } if err := verifyUserClaims(claims, &ci, &usr, issuerURL); err != nil { t.Fatalf("Failed to verify claims: %v", err) } }
func TestServerTokenFail(t *testing.T) { keyFixture := "goodkey" signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} tests := []struct { signer jose.Signer argCC oidc.ClientCredentials argKey string err error scope []string refreshToken string }{ // control test case to make sure fixtures check out { // NOTE(ericchiang): This test assumes that the database ID of the first // refresh token will be "1". signer: signerFixture, argCC: testClientCredentials, argKey: keyFixture, scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, // no 'offline_access' in 'scope', should get empty refresh token { signer: signerFixture, argCC: testClientCredentials, argKey: keyFixture, scope: []string{"openid"}, }, // unrecognized key { signer: signerFixture, argCC: testClientCredentials, argKey: "foo", err: oauth2.NewError(oauth2.ErrorInvalidGrant), scope: []string{"openid", "offline_access"}, }, // unrecognized client { signer: signerFixture, argCC: oidc.ClientCredentials{ID: "YYY"}, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorInvalidClient), scope: []string{"openid", "offline_access"}, }, // signing operation fails { signer: &StaticSigner{sig: nil, err: errors.New("fail")}, argCC: testClientCredentials, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorServerError), scope: []string{"openid", "offline_access"}, }, } for i, tt := range tests { f, err := makeTestFixtures() if err != nil { t.Fatalf("error making test fixtures: %v", err) } sm := f.sessionManager sm.GenerateCode = func() (string, error) { return keyFixture, nil } f.srv.RefreshTokenRepo = refreshtest.NewTestRefreshTokenRepo() f.srv.KeyManager = &StaticKeyManager{ signer: tt.signer, } sessionID, err := sm.NewSession(testConnectorID1, testClientID, "bogus", url.URL{}, "", false, tt.scope) if err != nil { t.Fatalf("Unexpected error: %v", err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Errorf("case %d: unexpected error: %v", i, err) continue } _, err = sm.AttachUser(sessionID, testUserID1) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } _, err = sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, token, expiresAt, err := f.srv.CodeToken(tt.argCC, tt.argKey) if token != tt.refreshToken { fmt.Printf("case %d: expect refresh token %q, got %q\n", i, tt.refreshToken, token) t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token) panic("") } if !reflect.DeepEqual(err, tt.err) { t.Errorf("case %d: expect %v, got %v", i, tt.err, err) } if err == nil && jwt == nil { t.Errorf("case %d: got nil JWT", i) } if err != nil && jwt != nil { t.Errorf("case %d: got non-nil JWT %v", i, jwt) } if err == nil && expiresAt.IsZero() { t.Errorf("case %d: got zero expiration time %v", i, expiresAt) } } }
func TestServerRefreshToken(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} clientA := client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "client.example.com", Path: "one/two/three"}, }, }, } clientB := client.Client{ Credentials: oidc.ClientCredentials{ ID: "example2.com", Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "example2.com", Path: "one/two/three"}, }, }, } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} // NOTE(ericchiang): These tests assume that the database ID of the first // refresh token will be "1". tests := []struct { token string clientID string // The client that associates with the token. creds oidc.ClientCredentials signer jose.Signer err error }{ // Everything is good. { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientA.Credentials, signerFixture, nil, }, // Invalid refresh token(malformatted). { "invalid-token", clientA.Credentials.ID, clientA.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid payload content). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-2"))), clientA.Credentials.ID, clientA.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid ID content). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientA.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid client(client is not associated with the token). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientB.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no client ID). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: "", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no such client). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: "AAA", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no secrets). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: testClientID}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(invalid secret). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: "bad-id", Secret: "bad-secret"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Signing operation fails. { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientA.Credentials, &StaticSigner{sig: nil, err: errors.New("fail")}, oauth2.NewError(oauth2.ErrorServerError), }, } for i, tt := range tests { km := &StaticKeyManager{ signer: tt.signer, } clients := []client.Client{ clientA, clientB, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } dbm := db.NewMemDB() clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } if _, err := refreshTokenRepo.Create("testid-1", tt.clientID); err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, err := srv.RefreshToken(tt.creds, tt.token) if !reflect.DeepEqual(err, tt.err) { t.Errorf("Case %d: expect: %v, got: %v", i, tt.err, err) } if jwt != nil { if string(jwt.Signature) != "beer" { t.Errorf("Case %d: expect signature: beer, got signature: %v", i, jwt.Signature) } claims, err := jwt.Claims() if err != nil { t.Errorf("Case %d: unexpected error: %v", i, err) } if claims["iss"] != issuerURL.String() || claims["sub"] != "testid-1" || claims["aud"] != testClientID { t.Errorf("Case %d: invalid claims: %v", i, claims) } } } // Test that we should return error when user cannot be found after // verifying the token. km := &StaticKeyManager{ signer: signerFixture, } clients := []client.Client{ clientA, clientB, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } dbm := db.NewMemDB() clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } // Create a user that will be removed later. if err := userRepo.Create(nil, user.User{ ID: "testid-2", Email: "*****@*****.**", }); err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } if _, err := refreshTokenRepo.Create("testid-2", clientA.Credentials.ID); err != nil { t.Fatalf("Unexpected error: %v", err) } // Recreate the user repo to remove the user we created. userRepo, err = makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv.UserRepo = userRepo _, err = srv.RefreshToken(clientA.Credentials, fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1")))) if !reflect.DeepEqual(err, oauth2.NewError(oauth2.ErrorServerError)) { t.Errorf("Expect: %v, got: %v", oauth2.NewError(oauth2.ErrorServerError), err) } }
func TestServerTokenFail(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} keyFixture := "goodkey" ccFixture := oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} tests := []struct { signer jose.Signer argCC oidc.ClientCredentials argKey string err error scope []string refreshToken string }{ // control test case to make sure fixtures check out { // NOTE(ericchiang): This test assumes that the database ID of the first // refresh token will be "1". signer: signerFixture, argCC: ccFixture, argKey: keyFixture, scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, // no 'offline_access' in 'scope', should get empty refresh token { signer: signerFixture, argCC: ccFixture, argKey: keyFixture, scope: []string{"openid"}, }, // unrecognized key { signer: signerFixture, argCC: ccFixture, argKey: "foo", err: oauth2.NewError(oauth2.ErrorInvalidGrant), scope: []string{"openid", "offline_access"}, }, // unrecognized client { signer: signerFixture, argCC: oidc.ClientCredentials{ID: "YYY"}, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorInvalidClient), scope: []string{"openid", "offline_access"}, }, // signing operation fails { signer: &StaticSigner{sig: nil, err: errors.New("fail")}, argCC: ccFixture, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorServerError), scope: []string{"openid", "offline_access"}, }, } for i, tt := range tests { sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) sm.GenerateCode = func() (string, error) { return keyFixture, nil } sessionID, err := sm.NewSession("connector_id", ccFixture.ID, "bogus", url.URL{}, "", false, tt.scope) if err != nil { t.Fatalf("Unexpected error: %v", err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Errorf("case %d: unexpected error: %v", i, err) continue } km := &StaticKeyManager{ signer: tt.signer, } clients := []client.Client{ client.Client{ Credentials: ccFixture, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, }, } dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } _, err = sm.AttachUser(sessionID, "testid-1") if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: issuerURL, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } _, err = sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, token, err := srv.CodeToken(tt.argCC, tt.argKey) if token != tt.refreshToken { fmt.Printf("case %d: expect refresh token %q, got %q\n", i, tt.refreshToken, token) t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token) panic("") } if !reflect.DeepEqual(err, tt.err) { t.Errorf("case %d: expect %v, got %v", i, tt.err, err) } if err == nil && jwt == nil { t.Errorf("case %d: got nil JWT", i) } if err != nil && jwt != nil { t.Errorf("case %d: got non-nil JWT %v", i, jwt) } } }
func TestServerCodeToken(t *testing.T) { ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, } clients := []client.Client{ci} dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } tests := []struct { scope []string refreshToken string }{ // No 'offline_access' in scope, should get empty refresh token. { scope: []string{"openid"}, refreshToken: "", }, // Have 'offline_access' in scope, should get non-empty refresh token. { // NOTE(ericchiang): This test assumes that the database ID of the first // refresh token will be "1". scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, } for i, tt := range tests { sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false, tt.scope) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } _, err = sm.AttachUser(sessionID, "testid-1") if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } key, err := sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } jwt, token, err := srv.CodeToken(ci.Credentials, key) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } if jwt == nil { t.Fatalf("case %d: expect non-nil jwt", i) } if token != tt.refreshToken { t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token) } } }
func makeTestFixturesWithOptions(options testFixtureOptions) (*testFixtures, error) { dbMap := db.NewMemDB() userRepo, err := db.NewUserRepoFromUsers(dbMap, testUsers) if err != nil { return nil, err } pwRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, testPasswordInfos) if err != nil { return nil, err } connConfigs := []connector.ConnectorConfig{ &connector.OIDCConnectorConfig{ ID: testConnectorIDOpenID, IssuerURL: testIssuerURL.String(), ClientID: "12345", ClientSecret: "567789", }, &connector.OIDCConnectorConfig{ ID: testConnectorIDOpenIDTrusted, IssuerURL: testIssuerURL.String(), ClientID: "12345-trusted", ClientSecret: "567789-trusted", TrustedEmailProvider: true, }, &connector.OIDCConnectorConfig{ ID: testConnectorID1, IssuerURL: testIssuerURL.String(), ClientID: testConnectorID1 + "_client_id", ClientSecret: testConnectorID1 + "_client_secret", TrustedEmailProvider: true, }, &connector.LocalConnectorConfig{ ID: testConnectorLocalID, }, } connCfgRepo := db.NewConnectorConfigRepo(dbMap) if err := connCfgRepo.Set(connConfigs); err != nil { return nil, err } userManager := usermanager.NewUserManager(userRepo, pwRepo, connCfgRepo, db.TransactionFactory(dbMap), usermanager.ManagerOptions{}) sessionManager := sessionmanager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) sessionManager.GenerateCode = sequentialGenerateCodeFunc() refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() emailer, err := email.NewTemplatizedEmailerFromGlobs( emailTemplatesLocation+"/*.txt", emailTemplatesLocation+"/*.html", &email.FakeEmailer{}, "*****@*****.**") if err != nil { return nil, err } var clients []client.LoadableClient if options.clients == nil { clients = testClients } else { clients = options.clients } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo, err := db.NewClientRepoFromClients(dbMap, clients) if err != nil { return nil, err } clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, time.Now().Add(time.Minute))) if err != nil { return nil, err } tpl, err := getTemplates("dex", "https://coreos.com", "https://coreos.com/assets/images/brand/coreos-mark-30px.png", true, templatesLocation) if err != nil { return nil, err } srv := &Server{ IssuerURL: testIssuerURL, SessionManager: sessionManager, ClientRepo: clientRepo, Templates: tpl, UserRepo: userRepo, PasswordInfoRepo: pwRepo, UserManager: userManager, ClientManager: clientManager, KeyManager: km, RefreshTokenRepo: refreshTokenRepo, } err = setTemplates(srv, tpl) if err != nil { return nil, err } for _, config := range connConfigs { if err := srv.AddConnector(config); err != nil { return nil, err } } srv.UserEmailer = useremail.NewUserEmailer(srv.UserRepo, srv.PasswordInfoRepo, srv.KeyManager.Signer, srv.SessionManager.ValidityWindow, srv.IssuerURL, emailer, srv.absURL(httpPathResetPassword), srv.absURL(httpPathEmailVerify), srv.absURL(httpPathAcceptInvitation), ) clientCreds := map[string]oidc.ClientCredentials{} for _, c := range clients { clientCreds[c.Client.Credentials.ID] = c.Client.Credentials } return &testFixtures{ srv: srv, redirectURL: testRedirectURL, userRepo: userRepo, sessionManager: sessionManager, emailer: emailer, clientRepo: clientRepo, clientManager: clientManager, clientCreds: clientCreds, }, nil }