259 lines
7.5 KiB
Go
259 lines
7.5 KiB
Go
|
|
package zitadel_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/base64"
|
||
|
|
"encoding/json"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"gateway/internal/library/zitadel"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestAuthorizeURL(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{
|
||
|
|
Issuer: testIssuerURL,
|
||
|
|
OAuthClientID: testClientID,
|
||
|
|
GoogleIdPID: "google-idp-1",
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
raw, err := client.AuthorizeURL("https://app.example.com/callback", "state-abc", "google")
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
u, err := url.Parse(raw)
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.Equal(t, "https", u.Scheme)
|
||
|
|
require.Equal(t, "zitadel.example.com", u.Host)
|
||
|
|
require.Equal(t, "/oauth/v2/authorize", u.Path)
|
||
|
|
|
||
|
|
q := u.Query()
|
||
|
|
require.Equal(t, testClientID, q.Get("client_id"))
|
||
|
|
require.Equal(t, "https://app.example.com/callback", q.Get("redirect_uri"))
|
||
|
|
require.Equal(t, "code", q.Get("response_type"))
|
||
|
|
require.Equal(t, "openid profile email", q.Get("scope"))
|
||
|
|
require.Equal(t, "state-abc", q.Get("state"))
|
||
|
|
require.Equal(t, "google-idp-1", q.Get("idp_id"))
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAuthorizeURLWithoutGoogleIdP(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{
|
||
|
|
Issuer: testIssuerURL,
|
||
|
|
OAuthClientID: testClientID,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
raw, err := client.AuthorizeURL("https://app.example.com/callback", "state-abc", "google")
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.NotContains(t, raw, "idp_id=")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAuthorizeURLRequiresClientAndParams(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{Issuer: testIssuerURL})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
_, err = client.AuthorizeURL("https://app/callback", "state", "google")
|
||
|
|
require.Error(t, err)
|
||
|
|
require.Contains(t, err.Error(), "oauth client id")
|
||
|
|
|
||
|
|
_, err = client.AuthorizeURL("", "state", "google")
|
||
|
|
require.Error(t, err)
|
||
|
|
|
||
|
|
var nilClient *zitadel.Client
|
||
|
|
_, err = nilClient.AuthorizeURL("https://app/callback", "state", "google")
|
||
|
|
require.ErrorIs(t, err, zitadel.ErrNotConfigured)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExchangeAuthorizationCode(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
var gotForm url.Values
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
assert.Equal(t, "/oauth/v2/token", r.URL.Path)
|
||
|
|
body, err := io.ReadAll(r.Body)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
gotForm, err = url.ParseQuery(string(body))
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
_, _ = w.Write([]byte(`{"access_token":"at","id_token":"id","expires_in":3600,"token_type":"Bearer"}`))
|
||
|
|
}))
|
||
|
|
t.Cleanup(srv.Close)
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{
|
||
|
|
Issuer: srv.URL,
|
||
|
|
OAuthClientID: testClientID,
|
||
|
|
OAuthClientSecret: testSecret,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
tok, err := client.ExchangeAuthorizationCode(
|
||
|
|
context.Background(),
|
||
|
|
"auth-code-1",
|
||
|
|
"https://app.example.com/callback",
|
||
|
|
)
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.Equal(t, "at", tok.AccessToken)
|
||
|
|
require.Equal(t, "id", tok.IDToken)
|
||
|
|
require.Equal(t, "authorization_code", gotForm.Get("grant_type"))
|
||
|
|
require.Equal(t, testClientID, gotForm.Get("client_id"))
|
||
|
|
require.Equal(t, testSecret, gotForm.Get("client_secret"))
|
||
|
|
require.Equal(t, "auth-code-1", gotForm.Get("code"))
|
||
|
|
require.Equal(t, "https://app.example.com/callback", gotForm.Get("redirect_uri"))
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExchangeAuthorizationCodeInvalid(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
http.Error(w, "invalid", http.StatusUnauthorized)
|
||
|
|
}))
|
||
|
|
t.Cleanup(srv.Close)
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{
|
||
|
|
Issuer: srv.URL,
|
||
|
|
OAuthClientID: testClientID,
|
||
|
|
OAuthClientSecret: testSecret,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
_, err = client.ExchangeAuthorizationCode(context.Background(), "bad", "https://app/callback")
|
||
|
|
require.ErrorIs(t, err, zitadel.ErrInvalidCredentials)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExchangeAuthorizationCodeRequiresParams(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{
|
||
|
|
Issuer: testIssuerURL,
|
||
|
|
OAuthClientID: testClientID,
|
||
|
|
OAuthClientSecret: testSecret,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
_, err = client.ExchangeAuthorizationCode(context.Background(), "", "https://app/callback")
|
||
|
|
require.Error(t, err)
|
||
|
|
|
||
|
|
var nilClient *zitadel.Client
|
||
|
|
_, err = nilClient.ExchangeAuthorizationCode(context.Background(), "code", "https://app/callback")
|
||
|
|
require.ErrorIs(t, err, zitadel.ErrNotConfigured)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFetchUserInfo(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
assert.Equal(t, "/oidc/v1/userinfo", r.URL.Path)
|
||
|
|
assert.Equal(t, "Bearer access-token", r.Header.Get("Authorization"))
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
_, _ = w.Write([]byte(`{
|
||
|
|
"sub":"zitadel-sub-2",
|
||
|
|
"email":"bob@example.com",
|
||
|
|
"email_verified":true,
|
||
|
|
"name":"Bob",
|
||
|
|
"locale":"zh-tw"
|
||
|
|
}`))
|
||
|
|
}))
|
||
|
|
t.Cleanup(srv.Close)
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{Issuer: srv.URL})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
claims, err := client.FetchUserInfo(context.Background(), "access-token")
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.Equal(t, "zitadel-sub-2", claims.Sub)
|
||
|
|
require.Equal(t, "bob@example.com", claims.Email)
|
||
|
|
require.True(t, claims.EmailVerified)
|
||
|
|
require.Equal(t, "Bob", claims.Name)
|
||
|
|
require.Equal(t, "zh-tw", claims.Locale)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFetchUserInfoUnauthorized(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
http.Error(w, "invalid", http.StatusUnauthorized)
|
||
|
|
}))
|
||
|
|
t.Cleanup(srv.Close)
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{Issuer: srv.URL})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
_, err = client.FetchUserInfo(context.Background(), "bad-token")
|
||
|
|
require.ErrorIs(t, err, zitadel.ErrInvalidCredentials)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFetchUserInfoMissingSub(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
_, _ = w.Write([]byte(`{"email":"nobody@example.com"}`))
|
||
|
|
}))
|
||
|
|
t.Cleanup(srv.Close)
|
||
|
|
|
||
|
|
client, err := zitadel.NewClient(zitadel.Conf{Issuer: srv.URL})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
_, err = client.FetchUserInfo(context.Background(), "access-token")
|
||
|
|
require.Error(t, err)
|
||
|
|
require.Contains(t, err.Error(), "missing sub")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseIDTokenClaims(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
payload, err := json.Marshal(map[string]any{
|
||
|
|
"sub": "sub-1",
|
||
|
|
"email": "alice@example.com",
|
||
|
|
"email_verified": true,
|
||
|
|
"name": "Alice",
|
||
|
|
"locale": "en-us",
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
raw := strings.Join([]string{
|
||
|
|
base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256"}`)),
|
||
|
|
base64.RawURLEncoding.EncodeToString(payload),
|
||
|
|
"signature",
|
||
|
|
}, ".")
|
||
|
|
|
||
|
|
claims, err := zitadel.ParseIDTokenClaims(raw)
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.Equal(t, "sub-1", claims.Sub)
|
||
|
|
require.Equal(t, "alice@example.com", claims.Email)
|
||
|
|
require.True(t, claims.EmailVerified)
|
||
|
|
require.Equal(t, "Alice", claims.Name)
|
||
|
|
require.Equal(t, "en-us", claims.Locale)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseIDTokenClaimsErrors(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
_, err := zitadel.ParseIDTokenClaims("")
|
||
|
|
require.Error(t, err)
|
||
|
|
|
||
|
|
_, err = zitadel.ParseIDTokenClaims("not-a-jwt")
|
||
|
|
require.Error(t, err)
|
||
|
|
require.Contains(t, err.Error(), "malformed")
|
||
|
|
|
||
|
|
payload := base64.RawURLEncoding.EncodeToString([]byte(`{"email":"x@example.com"}`))
|
||
|
|
raw := "header." + payload + ".sig"
|
||
|
|
_, err = zitadel.ParseIDTokenClaims(raw)
|
||
|
|
require.Error(t, err)
|
||
|
|
require.Contains(t, err.Error(), "missing sub")
|
||
|
|
}
|