mirror of
https://github.com/hanzozt/zt.git
synced 2026-10-30 15:04:55 +00:00
github.com/hanzozt-incubator does not exist — cf is a third-party dependency the rebrand should never have touched, and points at openziti-incubator/cf again. A rename put a literal space inside identifiers (Hanzo ZTOrg, ClaimAudienceHanzo ZT); only occurrences with an identifier character on one side are joined, the brand name in prose stays. foundation renamed errorz.ApiError.Code to AppCode and gave the old name to a Code() int32 method, so a stale .Code resolved to the method instead of failing loudly. rest_model.APIError.Code is a different field and keeps its name. The foundation self-replace pinned v2.0.86, a tag that still declared the openziti path, under a require of v2.0.92 that declares the hanzozt one. Co-authored-by: Hanzo Dev <[email protected]>
315 lines
12 KiB
Go
315 lines
12 KiB
Go
//go:build apitests
|
|
|
|
package tests
|
|
|
|
import (
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
service2 "github.com/hanzozt/edge-api/rest_client_api_client/service"
|
|
edge_apis "github.com/hanzozt/sdk-golang/edge-apis"
|
|
"github.com/hanzozt/zt/v2/common"
|
|
"github.com/hanzozt/zt/v2/controller/oidc_auth"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func Test_Authenticate_OIDC_Refresh(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
|
|
rpServer, err := newOidcTestRp(ctx.ApiHost)
|
|
ctx.Req.NoError(err)
|
|
|
|
rpServer.Start()
|
|
defer rpServer.Stop()
|
|
|
|
clientApiUrl, err := url.Parse("https://" + ctx.ApiHost + EdgeClientApiPath)
|
|
ctx.Req.NoError(err)
|
|
|
|
client := edge_apis.NewClientApiClient([]*url.URL{clientApiUrl}, ctx.ControllerConfig.Id.CA(), nil)
|
|
|
|
client.Credentials = edge_apis.NewUpdbCredentials(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password)
|
|
client.SetUseOidc(true)
|
|
|
|
t.Run("can authenticate to obtain a new token backed API Session", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
apiSession, err := client.Authenticate(client.Credentials, nil)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.IsType(&edge_apis.ApiSessionOidc{}, apiSession)
|
|
|
|
oidcApiSession := apiSession.(*edge_apis.ApiSessionOidc)
|
|
jwtParser := jwt.NewParser()
|
|
|
|
idClaims := &jwt.MapClaims{}
|
|
idToken, _, err := jwtParser.ParseUnverified(oidcApiSession.OidcTokens.IDToken, idClaims)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(idToken)
|
|
originalIdAudiences, err := idToken.Claims.GetAudience()
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.Len(originalIdAudiences, 1, "expected 1 audience")
|
|
|
|
accessClaims := &jwt.MapClaims{}
|
|
acccessToken, _, err := jwtParser.ParseUnverified(oidcApiSession.OidcTokens.AccessToken, accessClaims)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(acccessToken)
|
|
originalAccessAudiences, err := acccessToken.Claims.GetAudience()
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.Len(originalAccessAudiences, 1, "expected 1 audience")
|
|
ctx.Req.NotEmpty(originalAccessAudiences[0], "expected access audience to be a non-empty string")
|
|
|
|
refreshClaims := &jwt.MapClaims{}
|
|
refreshToken, _, err := jwtParser.ParseUnverified(oidcApiSession.OidcTokens.RefreshToken, refreshClaims)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(refreshToken)
|
|
originalRefreshAudiences, err := refreshToken.Claims.GetAudience()
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.Len(originalRefreshAudiences, 1, "expected 1 audience")
|
|
ctx.Req.NotEmpty(originalRefreshAudiences[0], "expected refresh audience to be a non-empty string")
|
|
|
|
ctx.Req.Equal(originalIdAudiences[0], originalAccessAudiences[0], "id token and access token audiences do not match, id: %s, access: %s", originalIdAudiences[0], originalAccessAudiences[0])
|
|
ctx.Req.Equal(originalAccessAudiences[0], originalRefreshAudiences[0], "access token and refresh token audiences do not match, access: %s, refresh: %s", originalAccessAudiences[0], originalRefreshAudiences[0])
|
|
|
|
originalAccessScopes := (*accessClaims)["scopes"]
|
|
ctx.Req.Len(originalAccessScopes, 2)
|
|
ctx.Req.Contains(originalAccessScopes, oidc.ScopeOpenID)
|
|
ctx.Req.Contains(originalAccessScopes, oidc.ScopeOfflineAccess)
|
|
|
|
originalRefreshScopes := (*refreshClaims)["scopes"]
|
|
ctx.Req.Len(originalRefreshScopes, 2)
|
|
ctx.Req.Contains(originalRefreshScopes, oidc.ScopeOpenID)
|
|
ctx.Req.Contains(originalRefreshScopes, oidc.ScopeOfflineAccess)
|
|
|
|
t.Run("can refresh the token backed API Session via Refresh Token Request", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
oidcApiSession := apiSession.(*edge_apis.ApiSessionOidc)
|
|
|
|
req := &oidc.RefreshTokenRequest{
|
|
RefreshToken: oidcApiSession.OidcTokens.RefreshToken,
|
|
ClientID: oidcApiSession.OidcTokens.IDTokenClaims.ClientID,
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess},
|
|
}
|
|
|
|
enc := oidc.NewEncoder()
|
|
dst := map[string][]string{}
|
|
err := enc.Encode(req, dst)
|
|
ctx.Req.NoError(err)
|
|
|
|
dst["grant_type"] = []string{string(req.GrantType())}
|
|
|
|
newTokens := &oidc.TokenExchangeResponse{}
|
|
cReq := ctx.newAnonymousClientApiRequest().SetHeader("content-type", oidc_auth.FormContentType).SetMultiValueFormData(dst).SetResult(newTokens)
|
|
|
|
resp, err := cReq.Post("https://" + ctx.ApiHost + "/oidc/oauth/token")
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(resp)
|
|
ctx.Req.Equal(200, resp.StatusCode(), "expected status %d, got %d, request: %s", 200, resp.StatusCode(), resp.String())
|
|
|
|
t.Run("the new token has the correct audience and scopes", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
newAccessClaims := &common.AccessClaims{}
|
|
newAccessToken, _, err := jwtParser.ParseUnverified(newTokens.AccessToken, newAccessClaims)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(newAccessToken)
|
|
newAccessAudiences, err := newAccessToken.Claims.GetAudience()
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.Len(newAccessAudiences, 1, "expected 1 audience")
|
|
ctx.Equal(originalAccessAudiences[0], newAccessAudiences[0])
|
|
|
|
newRefreshClaims := &jwt.MapClaims{}
|
|
newRefreshToken, _, err := jwtParser.ParseUnverified(newTokens.RefreshToken, newRefreshClaims)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(newRefreshToken)
|
|
newRefreshAudiences, err := newRefreshToken.Claims.GetAudience()
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.Len(newRefreshAudiences, 1, "expected 1 audience")
|
|
ctx.Equal(originalRefreshAudiences[0], newRefreshAudiences[0])
|
|
|
|
newAccessScopes := newAccessClaims.CustomClaims.Scopes
|
|
ctx.Req.Len(newAccessScopes, 2)
|
|
ctx.Req.Contains(newAccessScopes, oidc.ScopeOpenID)
|
|
ctx.Req.Contains(newAccessScopes, oidc.ScopeOfflineAccess)
|
|
|
|
newRefreshScopes := (*newRefreshClaims)["scopes"]
|
|
ctx.Req.Len(newRefreshScopes, 2)
|
|
ctx.Req.Contains(newRefreshScopes, oidc.ScopeOpenID)
|
|
ctx.Req.Contains(newRefreshScopes, oidc.ScopeOfflineAccess)
|
|
|
|
t.Run("the new token has the correct claims", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotEmpty(newAccessClaims.AuthenticatorId)
|
|
ctx.Req.False(newAccessClaims.IsCertExtendable)
|
|
ctx.Req.True(newAccessClaims.IsAdmin)
|
|
ctx.Req.NotEmpty(newAccessClaims.ApiSessionId)
|
|
ctx.Req.NotEmpty(newAccessClaims.JWTID)
|
|
ctx.Req.Equal(common.TokenTypeAccess, newAccessClaims.Type)
|
|
ctx.Req.NotEmpty(newAccessClaims.Subject)
|
|
})
|
|
})
|
|
|
|
t.Run("can use the new token backed API Session", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
newApiSession := &edge_apis.ApiSessionOidc{
|
|
OidcTokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: newTokens.AccessToken,
|
|
TokenType: newTokens.TokenType,
|
|
RefreshToken: newTokens.RefreshToken,
|
|
},
|
|
IDTokenClaims: oidcApiSession.OidcTokens.IDTokenClaims,
|
|
IDToken: oidcApiSession.OidcTokens.IDToken,
|
|
},
|
|
}
|
|
|
|
newApiSessionI := edge_apis.ApiSession(newApiSession)
|
|
ctx.testContextChanged(t)
|
|
client.ApiSession.Store(&newApiSessionI)
|
|
|
|
params := service2.NewListServicesParams()
|
|
result, err := client.API.Service.ListServices(params, nil)
|
|
ctx.Req.NoError(err)
|
|
ctx.NotNil(result)
|
|
})
|
|
})
|
|
|
|
t.Run("can not refresh the token backed API Session via Refresh Token Request with the wrong client id", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
oidcApiSession := apiSession.(*edge_apis.ApiSessionOidc)
|
|
|
|
req := &oidc.RefreshTokenRequest{
|
|
RefreshToken: oidcApiSession.OidcTokens.RefreshToken,
|
|
ClientID: "123",
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess},
|
|
}
|
|
|
|
enc := oidc.NewEncoder()
|
|
dst := map[string][]string{}
|
|
err := enc.Encode(req, dst)
|
|
ctx.Req.NoError(err)
|
|
|
|
dst["grant_type"] = []string{string(req.GrantType())}
|
|
|
|
newTokens := &oidc.TokenExchangeResponse{}
|
|
cReq := ctx.newAnonymousClientApiRequest().SetHeader("content-type", oidc_auth.FormContentType).SetMultiValueFormData(dst).SetResult(newTokens)
|
|
|
|
resp, err := cReq.Post("https://" + ctx.ApiHost + "/oidc/oauth/token")
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(resp)
|
|
ctx.Req.Equal(400, resp.StatusCode(), "expected status %d, got %d, request: %s", 200, resp.StatusCode(), resp.String())
|
|
})
|
|
|
|
t.Run("can not refresh the token backed API Session via Refresh Token Request by swapping client ids", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
oidcApiSession := apiSession.(*edge_apis.ApiSessionOidc)
|
|
|
|
clientId := common.ClaimAudienceHanzoZT
|
|
if oidcApiSession.OidcTokens.IDTokenClaims.ClientID == common.ClaimAudienceHanzoZT {
|
|
clientId = common.ClaimLegacyNative
|
|
}
|
|
req := &oidc.RefreshTokenRequest{
|
|
RefreshToken: oidcApiSession.OidcTokens.RefreshToken,
|
|
ClientID: clientId,
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess},
|
|
}
|
|
|
|
enc := oidc.NewEncoder()
|
|
dst := map[string][]string{}
|
|
err := enc.Encode(req, dst)
|
|
ctx.Req.NoError(err)
|
|
|
|
dst["grant_type"] = []string{string(req.GrantType())}
|
|
|
|
newTokens := &oidc.TokenExchangeResponse{}
|
|
cReq := ctx.newAnonymousClientApiRequest().SetHeader("content-type", oidc_auth.FormContentType).SetMultiValueFormData(dst).SetResult(newTokens)
|
|
|
|
resp, err := cReq.Post("https://" + ctx.ApiHost + "/oidc/oauth/token")
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(resp)
|
|
ctx.Req.Equal(400, resp.StatusCode(), "expected status %d, got %d, request: %s", 200, resp.StatusCode(), resp.String())
|
|
})
|
|
|
|
t.Run("can not refresh the token backed API Session via Refresh Token Request with invalid scopes", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
oidcApiSession := apiSession.(*edge_apis.ApiSessionOidc)
|
|
|
|
req := &oidc.RefreshTokenRequest{
|
|
RefreshToken: oidcApiSession.OidcTokens.RefreshToken,
|
|
ClientID: oidcApiSession.OidcTokens.IDTokenClaims.ClientID,
|
|
Scopes: []string{"myscope"},
|
|
}
|
|
|
|
enc := oidc.NewEncoder()
|
|
dst := map[string][]string{}
|
|
err := enc.Encode(req, dst)
|
|
ctx.Req.NoError(err)
|
|
|
|
dst["grant_type"] = []string{string(req.GrantType())}
|
|
|
|
newTokens := &oidc.TokenExchangeResponse{}
|
|
cReq := ctx.newAnonymousClientApiRequest().SetHeader("content-type", oidc_auth.FormContentType).SetMultiValueFormData(dst).SetResult(newTokens)
|
|
|
|
resp, err := cReq.Post("https://" + ctx.ApiHost + "/oidc/oauth/token")
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(resp)
|
|
ctx.Req.Equal(400, resp.StatusCode(), "expected status %d, got %d, request: %s", 200, resp.StatusCode(), resp.String())
|
|
})
|
|
|
|
t.Run("can refresh the token backed API Session via Token Exchange Request", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
newSession, err := client.API.RefreshApiSession(apiSession, client.HttpClient)
|
|
|
|
ctx.Req.NoError(err)
|
|
ctx.NotNil(newSession)
|
|
ctx.NotNil(newSession.GetToken())
|
|
ctx.NotEqual(newSession.GetToken(), apiSession.GetToken())
|
|
ctx.IsType(newSession, &edge_apis.ApiSessionOidc{})
|
|
|
|
newTokens := newSession.(*edge_apis.ApiSessionOidc)
|
|
|
|
t.Run("the new token has the correct audiences", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
|
|
newAccessClaims := &jwt.MapClaims{}
|
|
newAccessToken, _, err := jwtParser.ParseUnverified(newTokens.OidcTokens.AccessToken, newAccessClaims)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(newAccessToken)
|
|
newAccessAudiences, err := newAccessToken.Claims.GetAudience()
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.Len(newAccessAudiences, 1, "expected 1 audience")
|
|
ctx.Equal(originalAccessAudiences[0], newAccessAudiences[0])
|
|
|
|
newRefreshClaims := &jwt.MapClaims{}
|
|
newRefreshToken, _, err := jwtParser.ParseUnverified(newTokens.OidcTokens.RefreshToken, newRefreshClaims)
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.NotNil(newRefreshToken)
|
|
newRefreshAudiences, err := newRefreshToken.Claims.GetAudience()
|
|
ctx.Req.NoError(err)
|
|
ctx.Req.Len(newRefreshAudiences, 1, "expected 1 audience")
|
|
ctx.Equal(originalRefreshAudiences[0], newRefreshAudiences[0])
|
|
})
|
|
|
|
t.Run("can use the new token backed API Session", func(t *testing.T) {
|
|
ctx.testContextChanged(t)
|
|
client.ApiSession.Store(&newSession)
|
|
|
|
params := service2.NewListServicesParams()
|
|
result, err := client.API.Service.ListServices(params, nil)
|
|
ctx.Req.NoError(err)
|
|
ctx.NotNil(result)
|
|
})
|
|
})
|
|
})
|
|
|
|
}
|