mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-08 18:47:52 +00:00
feat: refresh token (#1728)
* begin refresh tokens * refresh tokens * list and revoke refresh tokens * handle remove * tests for refresh tokens * uniqueness and default expiration * rename oidc token methods * cleanup * migration version * Update internal/static/i18n/en.yaml Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * fixes * feat: update oidc pkg for refresh tokens Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
parent
bc21eeb114
commit
ec5020bebc
@ -224,6 +224,8 @@ API:
|
|||||||
DefaultLoginURL: $ZITADEL_ACCOUNTS/login?authRequestID=
|
DefaultLoginURL: $ZITADEL_ACCOUNTS/login?authRequestID=
|
||||||
DefaultAccessTokenLifetime: 12h
|
DefaultAccessTokenLifetime: 12h
|
||||||
DefaultIdTokenLifetime: 12h
|
DefaultIdTokenLifetime: 12h
|
||||||
|
DefaultRefreshTokenIdleExpiration: 720h #30d
|
||||||
|
DefaultRefreshTokenExpiration: 2160h #90d
|
||||||
SigningKeyAlgorithm: RS256
|
SigningKeyAlgorithm: RS256
|
||||||
UserAgentCookieConfig:
|
UserAgentCookieConfig:
|
||||||
Name: caos.zitadel.useragent
|
Name: caos.zitadel.useragent
|
||||||
|
@ -47,6 +47,36 @@ Returns the user sessions of the authorized user of the current useragent
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### ListMyRefreshTokens
|
||||||
|
|
||||||
|
> **rpc** ListMyRefreshTokens([ListMyRefreshTokensRequest](#listmyrefreshtokensrequest))
|
||||||
|
[ListMyRefreshTokensResponse](#listmyrefreshtokensresponse)
|
||||||
|
|
||||||
|
Returns the refresh tokens of the authorized user
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RevokeMyRefreshToken
|
||||||
|
|
||||||
|
> **rpc** RevokeMyRefreshToken([RevokeMyRefreshTokenRequest](#revokemyrefreshtokenrequest))
|
||||||
|
[RevokeMyRefreshTokenResponse](#revokemyrefreshtokenresponse)
|
||||||
|
|
||||||
|
Revokes a single refresh token of the authorized user by its (token) id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RevokeAllMyRefreshTokens
|
||||||
|
|
||||||
|
> **rpc** RevokeAllMyRefreshTokens([RevokeAllMyRefreshTokensRequest](#revokeallmyrefreshtokensrequest))
|
||||||
|
[RevokeAllMyRefreshTokensResponse](#revokeallmyrefreshtokensresponse)
|
||||||
|
|
||||||
|
Revokes all refresh tokens of the authorized user
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### UpdateMyUserName
|
### UpdateMyUserName
|
||||||
|
|
||||||
> **rpc** UpdateMyUserName([UpdateMyUserNameRequest](#updatemyusernamerequest))
|
> **rpc** UpdateMyUserName([UpdateMyUserNameRequest](#updatemyusernamerequest))
|
||||||
@ -636,6 +666,24 @@ This is an empty request
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### ListMyRefreshTokensRequest
|
||||||
|
This is an empty request
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### ListMyRefreshTokensResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| details | zitadel.v1.ListDetails | - | |
|
||||||
|
| result | repeated zitadel.user.v1.RefreshToken | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### ListMyUserChangesRequest
|
### ListMyUserChangesRequest
|
||||||
|
|
||||||
|
|
||||||
@ -868,6 +916,40 @@ This is an empty request
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RevokeAllMyRefreshTokensRequest
|
||||||
|
This is an empty request
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RevokeAllMyRefreshTokensResponse
|
||||||
|
This is an empty response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RevokeMyRefreshTokenRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RevokeMyRefreshTokenResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### SetMyEmailRequest
|
### SetMyEmailRequest
|
||||||
|
|
||||||
|
|
||||||
|
@ -241,6 +241,24 @@ this query is always equals
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RefreshToken
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| id | string | - | |
|
||||||
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
|
| client_id | string | - | |
|
||||||
|
| auth_time | google.protobuf.Timestamp | - | |
|
||||||
|
| idle_expiration | google.protobuf.Timestamp | - | |
|
||||||
|
| expiration | google.protobuf.Timestamp | - | |
|
||||||
|
| scopes | repeated string | - | |
|
||||||
|
| audience | repeated string | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### SearchQuery
|
### SearchQuery
|
||||||
|
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@ -16,7 +16,7 @@ require (
|
|||||||
github.com/allegro/bigcache v1.2.1
|
github.com/allegro/bigcache v1.2.1
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc
|
||||||
github.com/caos/logging v0.0.2
|
github.com/caos/logging v0.0.2
|
||||||
github.com/caos/oidc v0.14.7
|
github.com/caos/oidc v0.15.0
|
||||||
github.com/caos/orbos v1.5.14-0.20210428081839-983ffc569980
|
github.com/caos/orbos v1.5.14-0.20210428081839-983ffc569980
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.1.0
|
github.com/cockroachdb/cockroach-go/v2 v2.1.0
|
||||||
github.com/duo-labs/webauthn v0.0.0-20200714211715-1daaee874e43
|
github.com/duo-labs/webauthn v0.0.0-20200714211715-1daaee874e43
|
||||||
|
8
go.sum
8
go.sum
@ -137,8 +137,8 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW
|
|||||||
github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo=
|
github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo=
|
||||||
github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
|
github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
|
||||||
github.com/caos/oidc v0.14.4/go.mod h1:H5Y2zw3YIrWqQOoy0wcmZva2a66bumDyU2iOhXiM9uA=
|
github.com/caos/oidc v0.14.4/go.mod h1:H5Y2zw3YIrWqQOoy0wcmZva2a66bumDyU2iOhXiM9uA=
|
||||||
github.com/caos/oidc v0.14.7 h1:HDxQhEWIOdmp1MoJAj8aRMP62Cga4yAk5M9UEJtdS1Y=
|
github.com/caos/oidc v0.15.0 h1:lSykVX6yfUbWpJPAZ9/ZCuowo95h7AgfgPaC15lzf4Y=
|
||||||
github.com/caos/oidc v0.14.7/go.mod h1:JiK5RXSOgag66wiSOMEkS+yS4R46Baz6dGwfr60VfvI=
|
github.com/caos/oidc v0.15.0/go.mod h1:JiK5RXSOgag66wiSOMEkS+yS4R46Baz6dGwfr60VfvI=
|
||||||
github.com/caos/orbos v1.5.14-0.20210428081839-983ffc569980 h1:Fz0aYUwGMA2tsu5w7SryqFGjqGClJVHbyhBMT5SXtPU=
|
github.com/caos/orbos v1.5.14-0.20210428081839-983ffc569980 h1:Fz0aYUwGMA2tsu5w7SryqFGjqGClJVHbyhBMT5SXtPU=
|
||||||
github.com/caos/orbos v1.5.14-0.20210428081839-983ffc569980/go.mod h1:2I8oiZb5SMRm/qTLvwpSmdV0M6ex8J/UKyxUGfKaqJo=
|
github.com/caos/orbos v1.5.14-0.20210428081839-983ffc569980/go.mod h1:2I8oiZb5SMRm/qTLvwpSmdV0M6ex8J/UKyxUGfKaqJo=
|
||||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||||
@ -436,6 +436,7 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c
|
|||||||
github.com/googleinterns/cloud-operations-api-mock v0.0.0-20200709193332-a1e58c29bdd3 h1:eHv/jVY/JNop1xg2J9cBb4EzyMpWZoNCP1BslSAIkOI=
|
github.com/googleinterns/cloud-operations-api-mock v0.0.0-20200709193332-a1e58c29bdd3 h1:eHv/jVY/JNop1xg2J9cBb4EzyMpWZoNCP1BslSAIkOI=
|
||||||
github.com/googleinterns/cloud-operations-api-mock v0.0.0-20200709193332-a1e58c29bdd3/go.mod h1:h/KNeRx7oYU4SpA4SoY7W2/NxDKEEVuwA6j9A27L4OI=
|
github.com/googleinterns/cloud-operations-api-mock v0.0.0-20200709193332-a1e58c29bdd3/go.mod h1:h/KNeRx7oYU4SpA4SoY7W2/NxDKEEVuwA6j9A27L4OI=
|
||||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
|
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
|
||||||
@ -577,6 +578,7 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
|
|||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
|
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
|
||||||
@ -841,7 +843,9 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
|||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||||
|
58
internal/api/grpc/auth/refresh_token.go
Normal file
58
internal/api/grpc/auth/refresh_token.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
|
"github.com/caos/zitadel/internal/api/grpc/object"
|
||||||
|
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
|
||||||
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
|
"github.com/caos/zitadel/pkg/grpc/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) ListMyRefreshTokens(ctx context.Context, req *auth.ListMyRefreshTokensRequest) (*auth.ListMyRefreshTokensResponse, error) {
|
||||||
|
res, err := s.repo.SearchMyRefreshTokens(ctx, authz.GetCtxData(ctx).UserID, ListMyRefreshTokensRequestToModel(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &auth.ListMyRefreshTokensResponse{
|
||||||
|
Result: user_grpc.RefreshTokensToPb(res.Result),
|
||||||
|
Details: object.ToListDetails(
|
||||||
|
res.TotalResult,
|
||||||
|
res.Sequence,
|
||||||
|
res.Timestamp,
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RevokeMyRefreshToken(ctx context.Context, req *auth.RevokeMyRefreshTokenRequest) (*auth.RevokeMyRefreshTokenResponse, error) {
|
||||||
|
ctxData := authz.GetCtxData(ctx)
|
||||||
|
details, err := s.command.RevokeRefreshToken(ctx, ctxData.UserID, ctxData.ResourceOwner, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &auth.RevokeMyRefreshTokenResponse{
|
||||||
|
Details: object.DomainToChangeDetailsPb(details),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RevokeAllMyRefreshTokens(ctx context.Context, _ *auth.RevokeAllMyRefreshTokensRequest) (*auth.RevokeAllMyRefreshTokensResponse, error) {
|
||||||
|
ctxData := authz.GetCtxData(ctx)
|
||||||
|
res, err := s.repo.SearchMyRefreshTokens(ctx, ctxData.UserID, ListMyRefreshTokensRequestToModel(nil))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokenIDs := make([]string, len(res.Result))
|
||||||
|
for i, view := range res.Result {
|
||||||
|
tokenIDs[i] = view.ID
|
||||||
|
}
|
||||||
|
err = s.command.RevokeRefreshTokens(ctx, ctxData.UserID, ctxData.ResourceOwner, tokenIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &auth.RevokeAllMyRefreshTokensResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListMyRefreshTokensRequestToModel(_ *auth.ListMyRefreshTokensRequest) *model.RefreshTokenSearchRequest {
|
||||||
|
return &model.RefreshTokenSearchRequest{} //add sorting, queries, ... when possible
|
||||||
|
}
|
30
internal/api/grpc/user/refresh_token.go
Normal file
30
internal/api/grpc/user/refresh_token.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/api/grpc/object"
|
||||||
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
|
"github.com/caos/zitadel/pkg/grpc/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RefreshTokensToPb(refreshTokens []*model.RefreshTokenView) []*user.RefreshToken {
|
||||||
|
tokens := make([]*user.RefreshToken, len(refreshTokens))
|
||||||
|
for i, token := range refreshTokens {
|
||||||
|
tokens[i] = RefreshTokenToPb(token)
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshTokenToPb(token *model.RefreshTokenView) *user.RefreshToken {
|
||||||
|
return &user.RefreshToken{
|
||||||
|
Id: token.ID,
|
||||||
|
Details: object.ToViewDetailsPb(token.Sequence, token.CreationDate, token.ChangeDate, token.ResourceOwner),
|
||||||
|
ClientId: token.ClientID,
|
||||||
|
AuthTime: timestamppb.New(token.AuthTime),
|
||||||
|
IdleExpiration: timestamppb.New(token.IdleExpiration),
|
||||||
|
Expiration: timestamppb.New(token.Expiration),
|
||||||
|
Scopes: token.Scopes,
|
||||||
|
Audience: token.Audience,
|
||||||
|
}
|
||||||
|
}
|
@ -80,7 +80,7 @@ func (o *OPStorage) DeleteAuthRequest(ctx context.Context, id string) (err error
|
|||||||
return o.repo.DeleteAuthRequest(ctx, id)
|
return o.repo.DeleteAuthRequest(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OPStorage) CreateToken(ctx context.Context, req op.TokenRequest) (_ string, _ time.Time, err error) {
|
func (o *OPStorage) CreateAccessToken(ctx context.Context, req op.TokenRequest) (_ string, _ time.Time, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
var userAgentID, applicationID, userOrgID string
|
var userAgentID, applicationID, userOrgID string
|
||||||
@ -107,6 +107,37 @@ func grantsToScopes(grants []*grant_model.UserGrantView) []string {
|
|||||||
return scopes
|
return scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OPStorage) CreateAccessAndRefreshTokens(ctx context.Context, req op.TokenRequest, refreshToken string) (_, _ string, _ time.Time, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
var userAgentID, applicationID, userOrgID string
|
||||||
|
var authTime time.Time
|
||||||
|
var authMethodsReferences []string
|
||||||
|
authReq, ok := req.(*AuthRequest)
|
||||||
|
if ok {
|
||||||
|
userAgentID = authReq.AgentID
|
||||||
|
applicationID = authReq.ApplicationID
|
||||||
|
userOrgID = authReq.UserOrgID
|
||||||
|
authTime = authReq.AuthTime
|
||||||
|
authMethodsReferences = authReq.GetAMR()
|
||||||
|
}
|
||||||
|
resp, token, err := o.command.AddAccessAndRefreshToken(ctx, userOrgID, userAgentID, applicationID, req.GetSubject(),
|
||||||
|
refreshToken, req.GetAudience(), req.GetScopes(), authMethodsReferences, o.defaultAccessTokenLifetime,
|
||||||
|
o.defaultRefreshTokenIdleExpiration, o.defaultRefreshTokenExpiration, authTime) //PLANNED: lifetime from client
|
||||||
|
if err != nil {
|
||||||
|
return "", "", time.Time{}, err
|
||||||
|
}
|
||||||
|
return resp.TokenID, token, resp.Expiration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
|
||||||
|
tokenView, err := o.repo.RefreshTokenByID(ctx, refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return RefreshTokenRequestFromBusiness(tokenView), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID string) (err error) {
|
func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID string) (err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
@ -2,7 +2,6 @@ package oidc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/caos/zitadel/internal/domain"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -11,7 +10,9 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
http_utils "github.com/caos/zitadel/internal/api/http"
|
http_utils "github.com/caos/zitadel/internal/api/http"
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -255,3 +256,39 @@ func AMRFromMFAType(mfaType domain.MFAType) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RefreshTokenRequestFromBusiness(tokenView *model.RefreshTokenView) op.RefreshTokenRequest {
|
||||||
|
return &RefreshTokenRequest{tokenView}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenRequest struct {
|
||||||
|
*model.RefreshTokenView
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) GetAMR() []string {
|
||||||
|
return r.AuthMethodsReferences
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) GetAudience() []string {
|
||||||
|
return r.Audience
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) GetAuthTime() time.Time {
|
||||||
|
return r.AuthTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) GetClientID() string {
|
||||||
|
return r.ClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) GetScopes() []string {
|
||||||
|
return r.Scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) GetSubject() string {
|
||||||
|
return r.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) SetCurrentScopes(scopes oidc.Scopes) {
|
||||||
|
r.Scopes = scopes
|
||||||
|
}
|
||||||
|
@ -28,10 +28,12 @@ type OPHandlerConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StorageConfig struct {
|
type StorageConfig struct {
|
||||||
DefaultLoginURL string
|
DefaultLoginURL string
|
||||||
SigningKeyAlgorithm string
|
SigningKeyAlgorithm string
|
||||||
DefaultAccessTokenLifetime types.Duration
|
DefaultAccessTokenLifetime types.Duration
|
||||||
DefaultIdTokenLifetime types.Duration
|
DefaultIdTokenLifetime types.Duration
|
||||||
|
DefaultRefreshTokenIdleExpiration types.Duration
|
||||||
|
DefaultRefreshTokenExpiration types.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type EndpointConfig struct {
|
type EndpointConfig struct {
|
||||||
@ -49,13 +51,15 @@ type Endpoint struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OPStorage struct {
|
type OPStorage struct {
|
||||||
repo repository.Repository
|
repo repository.Repository
|
||||||
command *command.Commands
|
command *command.Commands
|
||||||
query *query.Queries
|
query *query.Queries
|
||||||
defaultLoginURL string
|
defaultLoginURL string
|
||||||
defaultAccessTokenLifetime time.Duration
|
defaultAccessTokenLifetime time.Duration
|
||||||
defaultIdTokenLifetime time.Duration
|
defaultIdTokenLifetime time.Duration
|
||||||
signingKeyAlgorithm string
|
signingKeyAlgorithm string
|
||||||
|
defaultRefreshTokenIdleExpiration time.Duration
|
||||||
|
defaultRefreshTokenExpiration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig *crypto.KeyConfig, localDevMode bool) op.OpenIDProvider {
|
func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig *crypto.KeyConfig, localDevMode bool) op.OpenIDProvider {
|
||||||
@ -94,13 +98,15 @@ func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.C
|
|||||||
|
|
||||||
func newStorage(config StorageConfig, command *command.Commands, query *query.Queries, repo repository.Repository) *OPStorage {
|
func newStorage(config StorageConfig, command *command.Commands, query *query.Queries, repo repository.Repository) *OPStorage {
|
||||||
return &OPStorage{
|
return &OPStorage{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
command: command,
|
command: command,
|
||||||
query: query,
|
query: query,
|
||||||
defaultLoginURL: config.DefaultLoginURL,
|
defaultLoginURL: config.DefaultLoginURL,
|
||||||
signingKeyAlgorithm: config.SigningKeyAlgorithm,
|
signingKeyAlgorithm: config.SigningKeyAlgorithm,
|
||||||
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime.Duration,
|
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime.Duration,
|
||||||
defaultIdTokenLifetime: config.DefaultIdTokenLifetime.Duration,
|
defaultIdTokenLifetime: config.DefaultIdTokenLifetime.Duration,
|
||||||
|
defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration.Duration,
|
||||||
|
defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration.Duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
package eventstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/logging"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/crypto"
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
|
usr_view "github.com/caos/zitadel/internal/user/repository/view"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||||
|
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||||
|
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshTokenRepo struct {
|
||||||
|
Eventstore v1.Eventstore
|
||||||
|
View *view.View
|
||||||
|
SearchLimit uint64
|
||||||
|
KeyAlgorithm crypto.EncryptionAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRepo) RefreshTokenByID(ctx context.Context, refreshToken string) (*usr_model.RefreshTokenView, error) {
|
||||||
|
userID, tokenID, token, err := domain.FromRefreshToken(refreshToken, r.KeyAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokenView, viewErr := r.View.RefreshTokenByID(tokenID)
|
||||||
|
if viewErr != nil && !errors.IsNotFound(viewErr) {
|
||||||
|
return nil, viewErr
|
||||||
|
}
|
||||||
|
if errors.IsNotFound(viewErr) {
|
||||||
|
tokenView = new(model.RefreshTokenView)
|
||||||
|
tokenView.ID = tokenID
|
||||||
|
tokenView.UserID = userID
|
||||||
|
}
|
||||||
|
|
||||||
|
events, esErr := r.getUserEvents(ctx, userID, tokenView.Sequence)
|
||||||
|
if errors.IsNotFound(viewErr) && len(events) == 0 {
|
||||||
|
return nil, errors.ThrowNotFound(nil, "EVENT-BHB52", "Errors.User.RefreshToken.Invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if esErr != nil {
|
||||||
|
logging.Log("EVENT-AE462").WithError(viewErr).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error retrieving new events")
|
||||||
|
return model.RefreshTokenViewToModel(tokenView), nil
|
||||||
|
}
|
||||||
|
viewToken := *tokenView
|
||||||
|
for _, event := range events {
|
||||||
|
err := tokenView.AppendEventIfMyRefreshToken(event)
|
||||||
|
if err != nil {
|
||||||
|
return model.RefreshTokenViewToModel(&viewToken), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !tokenView.Expiration.After(time.Now()) || tokenView.Token != token {
|
||||||
|
return nil, errors.ThrowNotFound(nil, "EVENT-5Bm9s", "Errors.User.RefreshToken.Invalid")
|
||||||
|
}
|
||||||
|
return model.RefreshTokenViewToModel(tokenView), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRepo) SearchMyRefreshTokens(ctx context.Context, userID string, request *usr_model.RefreshTokenSearchRequest) (*usr_model.RefreshTokenSearchResponse, error) {
|
||||||
|
err := request.EnsureLimit(r.SearchLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sequence, err := r.View.GetLatestRefreshTokenSequence()
|
||||||
|
logging.Log("EVENT-GBdn4").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Warn("could not read latest refresh token sequence")
|
||||||
|
request.Queries = append(request.Queries, &usr_model.RefreshTokenSearchQuery{Key: usr_model.RefreshTokenSearchKeyUserID, Method: domain.SearchMethodEquals, Value: userID})
|
||||||
|
tokens, count, err := r.View.SearchRefreshTokens(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &usr_model.RefreshTokenSearchResponse{
|
||||||
|
Offset: request.Offset,
|
||||||
|
Limit: request.Limit,
|
||||||
|
TotalResult: count,
|
||||||
|
Sequence: sequence.CurrentSequence,
|
||||||
|
Timestamp: sequence.LastSuccessfulSpoolerRun,
|
||||||
|
Result: model.RefreshTokenViewsToModel(tokens),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRepo) getUserEvents(ctx context.Context, userID string, sequence uint64) ([]*models.Event, error) {
|
||||||
|
query, err := usr_view.UserByIDQuery(userID, sequence)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.Eventstore.FilterEvents(ctx, query)
|
||||||
|
}
|
@ -69,6 +69,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
|
|||||||
newProjectRole(handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount, es}),
|
newProjectRole(handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount, es}),
|
||||||
newLabelPolicy(handler{view, bulkLimit, configs.cycleDuration("LabelPolicy"), errorCount, es}),
|
newLabelPolicy(handler{view, bulkLimit, configs.cycleDuration("LabelPolicy"), errorCount, es}),
|
||||||
newFeatures(handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
|
newFeatures(handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
|
||||||
|
newRefreshToken(handler{view, bulkLimit, configs.cycleDuration("RefreshToken"), errorCount, es}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
123
internal/auth/repository/eventsourcing/handler/refresh_token.go
Normal file
123
internal/auth/repository/eventsourcing/handler/refresh_token.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/caos/logging"
|
||||||
|
|
||||||
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||||
|
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
|
||||||
|
project_es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
||||||
|
user_repo "github.com/caos/zitadel/internal/repository/user"
|
||||||
|
user_es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||||
|
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
refreshTokenTable = "auth.refresh_tokens"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
handler
|
||||||
|
subscription *v1.Subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRefreshToken(
|
||||||
|
handler handler,
|
||||||
|
) *RefreshToken {
|
||||||
|
h := &RefreshToken{
|
||||||
|
handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.subscribe()
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) subscribe() {
|
||||||
|
t.subscription = t.es.Subscribe(t.AggregateTypes()...)
|
||||||
|
go func() {
|
||||||
|
for event := range t.subscription.Events {
|
||||||
|
query.ReduceEvent(t, event)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) ViewModel() string {
|
||||||
|
return refreshTokenTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) AggregateTypes() []es_models.AggregateType {
|
||||||
|
return []es_models.AggregateType{user_es_model.UserAggregate, project_es_model.ProjectAggregate}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) CurrentSequence() (uint64, error) {
|
||||||
|
sequence, err := t.view.GetLatestRefreshTokenSequence()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return sequence.CurrentSequence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) EventQuery() (*es_models.SearchQuery, error) {
|
||||||
|
sequence, err := t.view.GetLatestRefreshTokenSequence()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return es_models.NewSearchQuery().
|
||||||
|
AggregateTypeFilter(user_es_model.UserAggregate, project_es_model.ProjectAggregate).
|
||||||
|
LatestSequenceFilter(sequence.CurrentSequence), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) Reduce(event *es_models.Event) (err error) {
|
||||||
|
switch eventstore.EventType(event.Type) {
|
||||||
|
case user_repo.HumanRefreshTokenAddedType:
|
||||||
|
token := new(view_model.RefreshTokenView)
|
||||||
|
err := token.AppendEvent(event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.view.PutRefreshToken(token, event)
|
||||||
|
case user_repo.HumanRefreshTokenRenewedType:
|
||||||
|
e := new(user_repo.HumanRefreshTokenRenewedEvent)
|
||||||
|
if err := json.Unmarshal(event.Data, e); err != nil {
|
||||||
|
logging.Log("EVEN-DBbn4").WithError(err).Error("could not unmarshal event data")
|
||||||
|
return caos_errs.ThrowInternal(nil, "MODEL-BHn75", "could not unmarshal data")
|
||||||
|
}
|
||||||
|
token, err := t.view.RefreshTokenByID(e.TokenID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = token.AppendEvent(event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.view.PutRefreshToken(token, event)
|
||||||
|
case user_repo.HumanRefreshTokenRemovedType:
|
||||||
|
e := new(user_repo.HumanRefreshTokenRemovedEvent)
|
||||||
|
if err := json.Unmarshal(event.Data, e); err != nil {
|
||||||
|
logging.Log("EVEN-BDbh3").WithError(err).Error("could not unmarshal event data")
|
||||||
|
return caos_errs.ThrowInternal(nil, "MODEL-Bz653", "could not unmarshal data")
|
||||||
|
}
|
||||||
|
return t.view.DeleteRefreshToken(e.TokenID, event)
|
||||||
|
case user_repo.UserLockedType,
|
||||||
|
user_repo.UserDeactivatedType,
|
||||||
|
user_repo.UserRemovedType:
|
||||||
|
return t.view.DeleteUserRefreshTokens(event.AggregateID, event)
|
||||||
|
default:
|
||||||
|
return t.view.ProcessedRefreshTokenSequence(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) OnError(event *es_models.Event, err error) error {
|
||||||
|
logging.LogWithFields("SPOOL-3jkl4", "id", event.AggregateID).WithError(err).Warn("something went wrong in token handler")
|
||||||
|
return spooler.HandleError(event, err, t.view.GetLatestTokenFailedEvent, t.view.ProcessedTokenFailedEvent, t.view.ProcessedTokenSequence, t.errorCountUntilSkip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshToken) OnSuccess() error {
|
||||||
|
return spooler.HandleSuccess(t.view.UpdateTokenSpoolerRunTimestamp)
|
||||||
|
}
|
@ -36,6 +36,7 @@ type EsRepository struct {
|
|||||||
eventstore.UserRepo
|
eventstore.UserRepo
|
||||||
eventstore.AuthRequestRepo
|
eventstore.AuthRequestRepo
|
||||||
eventstore.TokenRepo
|
eventstore.TokenRepo
|
||||||
|
eventstore.RefreshTokenRepo
|
||||||
eventstore.KeyRepository
|
eventstore.KeyRepository
|
||||||
eventstore.ApplicationRepo
|
eventstore.ApplicationRepo
|
||||||
eventstore.UserSessionRepo
|
eventstore.UserSessionRepo
|
||||||
@ -110,6 +111,12 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
|
|||||||
View: view,
|
View: view,
|
||||||
Eventstore: es,
|
Eventstore: es,
|
||||||
},
|
},
|
||||||
|
eventstore.RefreshTokenRepo{
|
||||||
|
View: view,
|
||||||
|
Eventstore: es,
|
||||||
|
SearchLimit: conf.SearchLimit,
|
||||||
|
KeyAlgorithm: keyAlgorithm,
|
||||||
|
},
|
||||||
eventstore.KeyRepository{
|
eventstore.KeyRepository{
|
||||||
View: view,
|
View: view,
|
||||||
Commands: command,
|
Commands: command,
|
||||||
|
86
internal/auth/repository/eventsourcing/view/refresh_token.go
Normal file
86
internal/auth/repository/eventsourcing/view/refresh_token.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
|
user_model "github.com/caos/zitadel/internal/user/model"
|
||||||
|
usr_view "github.com/caos/zitadel/internal/user/repository/view"
|
||||||
|
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||||
|
"github.com/caos/zitadel/internal/view/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
refreshTokenTable = "auth.refresh_tokens"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *View) RefreshTokenByID(tokenID string) (*model.RefreshTokenView, error) {
|
||||||
|
return usr_view.RefreshTokenByID(v.Db, refreshTokenTable, tokenID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) RefreshTokensByUserID(userID string) ([]*model.RefreshTokenView, error) {
|
||||||
|
return usr_view.RefreshTokensByUserID(v.Db, refreshTokenTable, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) SearchRefreshTokens(request *user_model.RefreshTokenSearchRequest) ([]*model.RefreshTokenView, uint64, error) {
|
||||||
|
return usr_view.SearchRefreshTokens(v.Db, refreshTokenTable, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) PutRefreshToken(token *model.RefreshTokenView, event *models.Event) error {
|
||||||
|
err := usr_view.PutRefreshToken(v.Db, refreshTokenTable, token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return v.ProcessedTokenSequence(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) PutRefreshTokens(token []*model.RefreshTokenView, event *models.Event) error {
|
||||||
|
err := usr_view.PutRefreshTokens(v.Db, refreshTokenTable, token...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return v.ProcessedRefreshTokenSequence(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) DeleteRefreshToken(tokenID string, event *models.Event) error {
|
||||||
|
err := usr_view.DeleteRefreshToken(v.Db, refreshTokenTable, tokenID)
|
||||||
|
if err != nil && !errors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return v.ProcessedRefreshTokenSequence(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) DeleteUserRefreshTokens(userID string, event *models.Event) error {
|
||||||
|
err := usr_view.DeleteUserRefreshTokens(v.Db, refreshTokenTable, userID)
|
||||||
|
if err != nil && !errors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return v.ProcessedRefreshTokenSequence(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) DeleteApplicationRefreshTokens(event *models.Event, ids ...string) error {
|
||||||
|
err := usr_view.DeleteApplicationTokens(v.Db, refreshTokenTable, ids)
|
||||||
|
if err != nil && !errors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return v.ProcessedRefreshTokenSequence(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) GetLatestRefreshTokenSequence() (*repository.CurrentSequence, error) {
|
||||||
|
return v.latestSequence(refreshTokenTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) ProcessedRefreshTokenSequence(event *models.Event) error {
|
||||||
|
return v.saveCurrentSequence(refreshTokenTable, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) UpdateRefreshTokenSpoolerRunTimestamp() error {
|
||||||
|
return v.updateSpoolerRunSequence(refreshTokenTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) GetLatestRefreshTokenFailedEvent(sequence uint64) (*repository.FailedEvent, error) {
|
||||||
|
return v.latestFailedEvent(refreshTokenTable, sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) ProcessedRefreshTokenFailedEvent(failedEvent *repository.FailedEvent) error {
|
||||||
|
return v.saveFailedEvent(failedEvent)
|
||||||
|
}
|
12
internal/auth/repository/refresh_token.go
Normal file
12
internal/auth/repository/refresh_token.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshTokenRepository interface {
|
||||||
|
RefreshTokenByID(ctx context.Context, refreshToken string) (*model.RefreshTokenView, error)
|
||||||
|
SearchMyRefreshTokens(ctx context.Context, userID string, request *model.RefreshTokenSearchRequest) (*model.RefreshTokenSearchResponse, error)
|
||||||
|
}
|
@ -17,4 +17,5 @@ type Repository interface {
|
|||||||
OrgRepository
|
OrgRepository
|
||||||
IAMRepository
|
IAMRepository
|
||||||
FeaturesRepository
|
FeaturesRepository
|
||||||
|
RefreshTokenRepository
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,20 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/api/authz"
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
|
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
|
||||||
"github.com/caos/zitadel/internal/config/types"
|
"github.com/caos/zitadel/internal/config/types"
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/api/http"
|
"github.com/caos/zitadel/internal/api/http"
|
||||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||||
"github.com/caos/zitadel/internal/crypto"
|
"github.com/caos/zitadel/internal/crypto"
|
||||||
"github.com/caos/zitadel/internal/id"
|
"github.com/caos/zitadel/internal/id"
|
||||||
iam_repo "github.com/caos/zitadel/internal/repository/iam"
|
iam_repo "github.com/caos/zitadel/internal/repository/iam"
|
||||||
keypair "github.com/caos/zitadel/internal/repository/keypair"
|
"github.com/caos/zitadel/internal/repository/keypair"
|
||||||
"github.com/caos/zitadel/internal/repository/org"
|
"github.com/caos/zitadel/internal/repository/org"
|
||||||
proj_repo "github.com/caos/zitadel/internal/repository/project"
|
proj_repo "github.com/caos/zitadel/internal/repository/project"
|
||||||
usr_repo "github.com/caos/zitadel/internal/repository/user"
|
usr_repo "github.com/caos/zitadel/internal/repository/user"
|
||||||
|
@ -3,12 +3,14 @@ package command
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
"github.com/caos/zitadel/internal/repository/user"
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
@ -209,50 +211,57 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) AddUserToken(ctx context.Context, orgID, agentID, clientID, userID string, audience, scopes []string, lifetime time.Duration) (*domain.Token, error) {
|
func (c *Commands) AddUserToken(ctx context.Context, orgID, agentID, clientID, userID string, audience, scopes []string, lifetime time.Duration) (*domain.Token, error) {
|
||||||
if userID == "" {
|
if orgID == "" || userID == "" {
|
||||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-55n8M", "Errors.IDMissing")
|
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Dbge4", "Errors.IDMissing")
|
||||||
}
|
}
|
||||||
|
userWriteModel := NewUserWriteModel(userID, orgID)
|
||||||
existingUser, err := c.userWriteModelByID(ctx, userID, orgID)
|
event, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, audience, scopes, lifetime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !isUserStateExists(existingUser.UserState) {
|
_, err = c.eventstore.PushEvents(ctx, event)
|
||||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return accessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteModel, agentID, clientID string, audience, scopes []string, lifetime time.Duration) (*user.UserTokenAddedEvent, *domain.Token, error) {
|
||||||
|
err := c.eventstore.FilterToQueryReducer(ctx, userWriteModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !isUserStateExists(userWriteModel.UserState) {
|
||||||
|
return nil, nil, caos_errs.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound")
|
||||||
}
|
}
|
||||||
|
|
||||||
audience = domain.AddAudScopeToAudience(audience, scopes)
|
audience = domain.AddAudScopeToAudience(audience, scopes)
|
||||||
|
|
||||||
preferredLanguage := ""
|
preferredLanguage := ""
|
||||||
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, orgID)
|
existingHuman, err := c.getHumanWriteModelByID(ctx, userWriteModel.AggregateID, userWriteModel.ResourceOwner)
|
||||||
if existingHuman != nil {
|
if existingHuman != nil {
|
||||||
preferredLanguage = existingHuman.PreferredLanguage.String()
|
preferredLanguage = existingHuman.PreferredLanguage.String()
|
||||||
}
|
}
|
||||||
expiration := time.Now().UTC().Add(lifetime)
|
expiration := time.Now().UTC().Add(lifetime)
|
||||||
tokenID, err := c.idGenerator.Next()
|
tokenID, err := c.idGenerator.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
|
userAgg := UserAggregateFromWriteModel(&userWriteModel.WriteModel)
|
||||||
_, err = c.eventstore.PushEvents(ctx,
|
return user.NewUserTokenAddedEvent(ctx, userAgg, tokenID, clientID, agentID, preferredLanguage, audience, scopes, expiration),
|
||||||
user.NewUserTokenAddedEvent(ctx, userAgg, tokenID, clientID, agentID, preferredLanguage, audience, scopes, expiration))
|
&domain.Token{
|
||||||
if err != nil {
|
ObjectRoot: models.ObjectRoot{
|
||||||
return nil, err
|
AggregateID: userWriteModel.AggregateID,
|
||||||
}
|
},
|
||||||
|
TokenID: tokenID,
|
||||||
return &domain.Token{
|
UserAgentID: agentID,
|
||||||
ObjectRoot: models.ObjectRoot{
|
ApplicationID: clientID,
|
||||||
AggregateID: userID,
|
Audience: audience,
|
||||||
},
|
Scopes: scopes,
|
||||||
TokenID: tokenID,
|
Expiration: expiration,
|
||||||
UserAgentID: agentID,
|
PreferredLanguage: preferredLanguage,
|
||||||
ApplicationID: clientID,
|
}, nil
|
||||||
Audience: audience,
|
|
||||||
Scopes: scopes,
|
|
||||||
Expiration: expiration,
|
|
||||||
PreferredLanguage: preferredLanguage,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events []eventstore.EventPusher, _ *UserWriteModel, err error) {
|
func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events []eventstore.EventPusher, _ *UserWriteModel, err error) {
|
||||||
|
139
internal/command/user_human_refresh_token.go
Normal file
139
internal/command/user_human_refresh_token.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Commands) AddAccessAndRefreshToken(ctx context.Context, orgID, agentID, clientID, userID, refreshToken string, audience, scopes, authMethodsReferences []string, accessLifetime, refreshIdleExpiration, refreshExpiration time.Duration, authTime time.Time) (*domain.Token, string, error) {
|
||||||
|
userWriteModel := NewUserWriteModel(userID, orgID)
|
||||||
|
accessTokenEvent, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, audience, scopes, accessLifetime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
creator := func() (eventstore.EventPusher, string, error) {
|
||||||
|
return c.addRefreshToken(ctx, accessToken, authMethodsReferences, authTime, refreshIdleExpiration, refreshExpiration)
|
||||||
|
}
|
||||||
|
if refreshToken != "" {
|
||||||
|
creator = func() (eventstore.EventPusher, string, error) {
|
||||||
|
return c.renewRefreshToken(ctx, userID, orgID, refreshToken, refreshIdleExpiration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshTokenEvent, token, err := creator()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
_, err = c.eventstore.PushEvents(ctx, accessTokenEvent, refreshTokenEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return accessToken, token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addRefreshToken(ctx context.Context, accessToken *domain.Token, authMethodsReferences []string, authTime time.Time, idleExpiration, expiration time.Duration) (*user.HumanRefreshTokenAddedEvent, string, error) {
|
||||||
|
tokenID, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
refreshToken, err := domain.NewRefreshToken(accessToken.AggregateID, tokenID, c.keyAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
refreshTokenWriteModel := NewHumanRefreshTokenWriteModel(accessToken.AggregateID, accessToken.ResourceOwner, tokenID)
|
||||||
|
userAgg := UserAggregateFromWriteModel(&refreshTokenWriteModel.WriteModel)
|
||||||
|
return user.NewHumanRefreshTokenAddedEvent(ctx, userAgg, tokenID, accessToken.ApplicationID, accessToken.UserAgentID,
|
||||||
|
accessToken.PreferredLanguage, accessToken.Audience, accessToken.Scopes, authMethodsReferences, authTime, idleExpiration, expiration),
|
||||||
|
refreshToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) renewRefreshToken(ctx context.Context, userID, orgID, refreshToken string, idleExpiration time.Duration) (event *user.HumanRefreshTokenRenewedEvent, newRefreshToken string, err error) {
|
||||||
|
if refreshToken == "" {
|
||||||
|
return nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-DHrr3", "Errors.IDMissing")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenUserID, tokenID, token, err := domain.FromRefreshToken(refreshToken, c.keyAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", caos_errs.ThrowInvalidArgument(err, "COMMAND-Dbfe4", "Errors.User.RefreshToken.Invalid")
|
||||||
|
}
|
||||||
|
if tokenUserID != userID {
|
||||||
|
return nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-Ht2g2", "Errors.User.RefreshToken.Invalid")
|
||||||
|
}
|
||||||
|
refreshTokenWriteModel := NewHumanRefreshTokenWriteModel(userID, orgID, tokenID)
|
||||||
|
err = c.eventstore.FilterToQueryReducer(ctx, refreshTokenWriteModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if refreshTokenWriteModel.UserState != domain.UserStateActive {
|
||||||
|
return nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-BHnhs", "Errors.User.RefreshToken.Invalid")
|
||||||
|
}
|
||||||
|
if refreshTokenWriteModel.RefreshToken != token ||
|
||||||
|
refreshTokenWriteModel.IdleExpiration.Before(time.Now()) ||
|
||||||
|
refreshTokenWriteModel.Expiration.Before(time.Now()) {
|
||||||
|
return nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-Vr43e", "Errors.User.RefreshToken.Invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
newToken, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
newRefreshToken, err = domain.RefreshToken(userID, tokenID, newToken, c.keyAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
userAgg := UserAggregateFromWriteModel(&refreshTokenWriteModel.WriteModel)
|
||||||
|
return user.NewHumanRefreshTokenRenewedEvent(ctx, userAgg, tokenID, newToken, idleExpiration), newRefreshToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) RevokeRefreshToken(ctx context.Context, userID, orgID, tokenID string) (*domain.ObjectDetails, error) {
|
||||||
|
removeEvent, refreshTokenWriteModel, err := c.removeRefreshToken(ctx, userID, orgID, tokenID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events, err := c.eventstore.PushEvents(ctx, removeEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(refreshTokenWriteModel, events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writeModelToObjectDetails(&refreshTokenWriteModel.WriteModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) RevokeRefreshTokens(ctx context.Context, userID, orgID string, tokenIDs []string) (err error) {
|
||||||
|
if len(tokenIDs) == 0 {
|
||||||
|
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Gfj42", "Errors.IDMissing")
|
||||||
|
}
|
||||||
|
events := make([]eventstore.EventPusher, len(tokenIDs))
|
||||||
|
for i, tokenID := range tokenIDs {
|
||||||
|
event, _, err := c.removeRefreshToken(ctx, userID, orgID, tokenID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
events[i] = event
|
||||||
|
}
|
||||||
|
_, err = c.eventstore.PushEvents(ctx, events...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) removeRefreshToken(ctx context.Context, userID, orgID, tokenID string) (*user.HumanRefreshTokenRemovedEvent, *HumanRefreshTokenWriteModel, error) {
|
||||||
|
if userID == "" || orgID == "" || tokenID == "" {
|
||||||
|
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-GVDgf", "Errors.IDMissing")
|
||||||
|
}
|
||||||
|
refreshTokenWriteModel := NewHumanRefreshTokenWriteModel(userID, orgID, tokenID)
|
||||||
|
err := c.eventstore.FilterToQueryReducer(ctx, refreshTokenWriteModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if refreshTokenWriteModel.UserState != domain.UserStateActive {
|
||||||
|
return nil, nil, caos_errs.ThrowNotFound(nil, "COMMAND-BHt2w", "Errors.User.RefreshToken.NotFound")
|
||||||
|
}
|
||||||
|
userAgg := UserAggregateFromWriteModel(&refreshTokenWriteModel.WriteModel)
|
||||||
|
return user.NewHumanRefreshTokenRemovedEvent(ctx, userAgg, tokenID), refreshTokenWriteModel, nil
|
||||||
|
}
|
89
internal/command/user_human_refresh_token_model.go
Normal file
89
internal/command/user_human_refresh_token_model.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HumanRefreshTokenWriteModel struct {
|
||||||
|
eventstore.WriteModel
|
||||||
|
|
||||||
|
TokenID string
|
||||||
|
RefreshToken string
|
||||||
|
|
||||||
|
UserState domain.UserState
|
||||||
|
IdleExpiration time.Time
|
||||||
|
Expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHumanRefreshTokenWriteModel(userID, resourceOwner, tokenID string) *HumanRefreshTokenWriteModel {
|
||||||
|
return &HumanRefreshTokenWriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: userID,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
TokenID: tokenID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *HumanRefreshTokenWriteModel) AppendEvents(events ...eventstore.EventReader) {
|
||||||
|
for _, event := range events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *user.HumanRefreshTokenAddedEvent:
|
||||||
|
if wm.TokenID != e.TokenID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *user.HumanRefreshTokenRenewedEvent:
|
||||||
|
if wm.TokenID != e.TokenID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *user.HumanRefreshTokenRemovedEvent:
|
||||||
|
if wm.TokenID != e.TokenID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *HumanRefreshTokenWriteModel) Reduce() error {
|
||||||
|
for _, event := range wm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *user.HumanRefreshTokenAddedEvent:
|
||||||
|
wm.TokenID = e.TokenID
|
||||||
|
wm.RefreshToken = e.TokenID
|
||||||
|
wm.IdleExpiration = e.CreationDate().Add(e.IdleExpiration)
|
||||||
|
wm.Expiration = e.CreationDate().Add(e.Expiration)
|
||||||
|
wm.UserState = domain.UserStateActive
|
||||||
|
case *user.HumanRefreshTokenRenewedEvent:
|
||||||
|
if wm.UserState == domain.UserStateActive {
|
||||||
|
wm.RefreshToken = e.RefreshToken
|
||||||
|
}
|
||||||
|
wm.RefreshToken = e.RefreshToken
|
||||||
|
wm.IdleExpiration = e.CreationDate().Add(e.IdleExpiration)
|
||||||
|
case *user.HumanRefreshTokenRemovedEvent:
|
||||||
|
wm.UserState = domain.UserStateDeleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wm.WriteModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *HumanRefreshTokenWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
|
||||||
|
AggregateIDs(wm.AggregateID).
|
||||||
|
EventTypes(
|
||||||
|
user.HumanRefreshTokenAddedType,
|
||||||
|
user.HumanRefreshTokenRenewedType,
|
||||||
|
user.HumanRefreshTokenRemovedType,
|
||||||
|
user.UserRemovedType)
|
||||||
|
if wm.ResourceOwner != "" {
|
||||||
|
query.ResourceOwner(wm.ResourceOwner)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
1103
internal/command/user_human_refresh_token_test.go
Normal file
1103
internal/command/user_human_refresh_token_test.go
Normal file
File diff suppressed because it is too large
Load Diff
37
internal/domain/refresh_token.go
Normal file
37
internal/domain/refresh_token.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/crypto"
|
||||||
|
caos_errors "github.com/caos/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRefreshToken(userID, tokenID string, algorithm crypto.EncryptionAlgorithm) (string, error) {
|
||||||
|
return RefreshToken(userID, tokenID, tokenID, algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshToken(userID, tokenID, token string, algorithm crypto.EncryptionAlgorithm) (string, error) {
|
||||||
|
encrypted, err := algorithm.Encrypt([]byte(userID + ":" + tokenID + ":" + token))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(encrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromRefreshToken(refreshToken string, algorithm crypto.EncryptionAlgorithm) (userID, tokenID, token string, err error) {
|
||||||
|
decoded, err := base64.RawURLEncoding.DecodeString(refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", err
|
||||||
|
}
|
||||||
|
decrypted, err := algorithm.Decrypt(decoded, algorithm.EncryptionKeyID())
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", err
|
||||||
|
}
|
||||||
|
split := strings.Split(string(decrypted), ":")
|
||||||
|
if len(split) != 3 {
|
||||||
|
return "", "", "", caos_errors.ThrowInternal(nil, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid")
|
||||||
|
}
|
||||||
|
return split[0], split[1], split[2], nil
|
||||||
|
}
|
@ -94,6 +94,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
|||||||
RegisterFilterEventMapper(HumanPasswordlessTokenBeginLoginType, HumanPasswordlessBeginLoginEventMapper).
|
RegisterFilterEventMapper(HumanPasswordlessTokenBeginLoginType, HumanPasswordlessBeginLoginEventMapper).
|
||||||
RegisterFilterEventMapper(HumanPasswordlessTokenCheckSucceededType, HumanPasswordlessCheckSucceededEventMapper).
|
RegisterFilterEventMapper(HumanPasswordlessTokenCheckSucceededType, HumanPasswordlessCheckSucceededEventMapper).
|
||||||
RegisterFilterEventMapper(HumanPasswordlessTokenCheckFailedType, HumanPasswordlessCheckFailedEventMapper).
|
RegisterFilterEventMapper(HumanPasswordlessTokenCheckFailedType, HumanPasswordlessCheckFailedEventMapper).
|
||||||
|
RegisterFilterEventMapper(HumanRefreshTokenAddedType, HumanRefreshTokenAddedEventMapper).
|
||||||
|
RegisterFilterEventMapper(HumanRefreshTokenRenewedType, HumanRefreshTokenRenewedEventEventMapper).
|
||||||
|
RegisterFilterEventMapper(HumanRefreshTokenRemovedType, HumanRefreshTokenRemovedEventEventMapper).
|
||||||
RegisterFilterEventMapper(MachineAddedEventType, MachineAddedEventMapper).
|
RegisterFilterEventMapper(MachineAddedEventType, MachineAddedEventMapper).
|
||||||
RegisterFilterEventMapper(MachineChangedEventType, MachineChangedEventMapper).
|
RegisterFilterEventMapper(MachineChangedEventType, MachineChangedEventMapper).
|
||||||
RegisterFilterEventMapper(MachineKeyAddedEventType, MachineKeyAddedEventMapper).
|
RegisterFilterEventMapper(MachineKeyAddedEventType, MachineKeyAddedEventMapper).
|
||||||
|
187
internal/repository/user/human_refresh_token.go
Normal file
187
internal/repository/user/human_refresh_token.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
refreshTokenEventPrefix = humanEventPrefix + "refresh.token."
|
||||||
|
HumanRefreshTokenAddedType = refreshTokenEventPrefix + "added"
|
||||||
|
HumanRefreshTokenRenewedType = refreshTokenEventPrefix + "renewed"
|
||||||
|
HumanRefreshTokenRemovedType = refreshTokenEventPrefix + "removed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HumanRefreshTokenAddedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
TokenID string `json:"tokenId"`
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
UserAgentID string `json:"userAgentId"`
|
||||||
|
Audience []string `json:"audience"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
AuthMethodsReferences []string `json:"authMethodReferences"`
|
||||||
|
AuthTime time.Time `json:"authTime"`
|
||||||
|
IdleExpiration time.Duration `json:"idleExpiration"`
|
||||||
|
Expiration time.Duration `json:"expiration"`
|
||||||
|
PreferredLanguage string `json:"preferredLanguage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenAddedEvent) Data() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenAddedEvent) Assets() []*eventstore.Asset {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHumanRefreshTokenAddedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
tokenID,
|
||||||
|
clientID,
|
||||||
|
userAgentID,
|
||||||
|
preferredLanguage string,
|
||||||
|
audience,
|
||||||
|
scopes,
|
||||||
|
authMethodsReferences []string,
|
||||||
|
authTime time.Time,
|
||||||
|
idleExpiration,
|
||||||
|
expiration time.Duration,
|
||||||
|
) *HumanRefreshTokenAddedEvent {
|
||||||
|
return &HumanRefreshTokenAddedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
HumanRefreshTokenAddedType,
|
||||||
|
),
|
||||||
|
TokenID: tokenID,
|
||||||
|
ClientID: clientID,
|
||||||
|
UserAgentID: userAgentID,
|
||||||
|
Audience: audience,
|
||||||
|
Scopes: scopes,
|
||||||
|
AuthMethodsReferences: authMethodsReferences,
|
||||||
|
AuthTime: authTime,
|
||||||
|
IdleExpiration: idleExpiration,
|
||||||
|
Expiration: expiration,
|
||||||
|
PreferredLanguage: preferredLanguage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HumanRefreshTokenAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||||
|
refreshTokenAdded := &HumanRefreshTokenAddedEvent{
|
||||||
|
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(event.Data, refreshTokenAdded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "USER-DGr14", "unable to unmarshal refresh token added")
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshTokenAdded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type HumanRefreshTokenRenewedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
TokenID string `json:"tokenId"`
|
||||||
|
RefreshToken string `json:"refreshToken"`
|
||||||
|
IdleExpiration time.Duration `json:"idleExpiration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenRenewedEvent) Data() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenRenewedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenRenewedEvent) Assets() []*eventstore.Asset {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHumanRefreshTokenRenewedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
tokenID,
|
||||||
|
refreshToken string,
|
||||||
|
idleExpiration time.Duration,
|
||||||
|
) *HumanRefreshTokenRenewedEvent {
|
||||||
|
return &HumanRefreshTokenRenewedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
HumanRefreshTokenRenewedType,
|
||||||
|
),
|
||||||
|
TokenID: tokenID,
|
||||||
|
IdleExpiration: idleExpiration,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HumanRefreshTokenRenewedEventEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||||
|
tokenAdded := &HumanRefreshTokenRenewedEvent{
|
||||||
|
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(event.Data, tokenAdded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "USER-GBt21", "unable to unmarshal refresh token renewed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenAdded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type HumanRefreshTokenRemovedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
TokenID string `json:"tokenId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenRemovedEvent) Data() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HumanRefreshTokenRemovedEvent) Assets() []*eventstore.Asset {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHumanRefreshTokenRemovedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
tokenID string,
|
||||||
|
) *HumanRefreshTokenRemovedEvent {
|
||||||
|
return &HumanRefreshTokenRemovedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
HumanRefreshTokenRemovedType,
|
||||||
|
),
|
||||||
|
TokenID: tokenID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HumanRefreshTokenRemovedEventEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||||
|
tokenAdded := &HumanRefreshTokenRemovedEvent{
|
||||||
|
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(event.Data, tokenAdded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "USER-Dggs2", "unable to unmarshal refresh token removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenAdded, nil
|
||||||
|
}
|
@ -3,9 +3,10 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||||
)
|
)
|
||||||
@ -202,7 +203,7 @@ type UserTokenAddedEvent struct {
|
|||||||
ApplicationID string `json:"applicationId"`
|
ApplicationID string `json:"applicationId"`
|
||||||
UserAgentID string `json:"userAgentId"`
|
UserAgentID string `json:"userAgentId"`
|
||||||
Audience []string `json:"audience"`
|
Audience []string `json:"audience"`
|
||||||
Scopes []string `json:"scopes""`
|
Scopes []string `json:"scopes"`
|
||||||
Expiration time.Time `json:"expiration"`
|
Expiration time.Time `json:"expiration"`
|
||||||
PreferredLanguage string `json:"preferredLanguage"`
|
PreferredLanguage string `json:"preferredLanguage"`
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,9 @@ Errors:
|
|||||||
BeginLoginFailed: Es ist ein Fehler beim WebAuthN Login aufgetreten
|
BeginLoginFailed: Es ist ein Fehler beim WebAuthN Login aufgetreten
|
||||||
ValidateLoginFailed: Zugangsdaten konnten nicht validiert werden
|
ValidateLoginFailed: Zugangsdaten konnten nicht validiert werden
|
||||||
CloneWarning: Authentifizierungsdaten wurden möglicherweise geklont
|
CloneWarning: Authentifizierungsdaten wurden möglicherweise geklont
|
||||||
|
RefreshToken:
|
||||||
|
Invalid: Refresh Token ist ungültig
|
||||||
|
NotFound: Refresh Token nicht gefunden
|
||||||
Org:
|
Org:
|
||||||
AlreadyExist: Organisationsname existiert bereits
|
AlreadyExist: Organisationsname existiert bereits
|
||||||
Invalid: Organisation ist ungültig
|
Invalid: Organisation ist ungültig
|
||||||
|
@ -108,6 +108,9 @@ Errors:
|
|||||||
BeginLoginFailed: WebAuthN begin login failed
|
BeginLoginFailed: WebAuthN begin login failed
|
||||||
ValidateLoginFailed: Error on validate login credentials
|
ValidateLoginFailed: Error on validate login credentials
|
||||||
CloneWarning: Credentials may be cloned
|
CloneWarning: Credentials may be cloned
|
||||||
|
RefreshToken:
|
||||||
|
Invalid: Refresh Token is invalid
|
||||||
|
NotFound: Refresh Token not found
|
||||||
Org:
|
Org:
|
||||||
AlreadyExist: Organisationname already taken
|
AlreadyExist: Organisationname already taken
|
||||||
Invalid: Organisation is invalid
|
Invalid: Organisation is invalid
|
||||||
|
18
internal/user/model/refresh_token.go
Normal file
18
internal/user/model/refresh_token.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
es_models.ObjectRoot
|
||||||
|
|
||||||
|
TokenID string
|
||||||
|
ApplicationID string
|
||||||
|
UserAgentID string
|
||||||
|
Audience []string
|
||||||
|
Expiration time.Time
|
||||||
|
Scopes []string
|
||||||
|
PreferredLanguage string
|
||||||
|
}
|
71
internal/user/model/refresh_token_view.go
Normal file
71
internal/user/model/refresh_token_view.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
caos_errors "github.com/caos/zitadel/internal/errors"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshTokenView struct {
|
||||||
|
ID string
|
||||||
|
CreationDate time.Time
|
||||||
|
ChangeDate time.Time
|
||||||
|
ResourceOwner string
|
||||||
|
UserID string
|
||||||
|
ClientID string
|
||||||
|
UserAgentID string
|
||||||
|
AuthMethodsReferences []string
|
||||||
|
Audience []string
|
||||||
|
AuthTime time.Time
|
||||||
|
IdleExpiration time.Time
|
||||||
|
Expiration time.Time
|
||||||
|
Scopes []string
|
||||||
|
Sequence uint64
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenSearchRequest struct {
|
||||||
|
Offset uint64
|
||||||
|
Limit uint64
|
||||||
|
SortingColumn RefreshTokenSearchKey
|
||||||
|
Asc bool
|
||||||
|
Queries []*RefreshTokenSearchQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenSearchKey int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
RefreshTokenSearchKeyUnspecified RefreshTokenSearchKey = iota
|
||||||
|
RefreshTokenSearchKeyRefreshTokenID
|
||||||
|
RefreshTokenSearchKeyUserID
|
||||||
|
RefreshTokenSearchKeyApplicationID
|
||||||
|
RefreshTokenSearchKeyUserAgentID
|
||||||
|
RefreshTokenSearchKeyExpiration
|
||||||
|
RefreshTokenSearchKeyResourceOwner
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshTokenSearchQuery struct {
|
||||||
|
Key RefreshTokenSearchKey
|
||||||
|
Method domain.SearchMethod
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenSearchResponse struct {
|
||||||
|
Offset uint64
|
||||||
|
Limit uint64
|
||||||
|
TotalResult uint64
|
||||||
|
Sequence uint64
|
||||||
|
Timestamp time.Time
|
||||||
|
Result []*RefreshTokenView
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenSearchRequest) EnsureLimit(limit uint64) error {
|
||||||
|
if r.Limit > limit {
|
||||||
|
return caos_errors.ThrowInvalidArgument(nil, "SEARCH-M0fse", "Errors.Limit.ExceedsDefault")
|
||||||
|
}
|
||||||
|
if r.Limit == 0 {
|
||||||
|
r.Limit = limit
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
154
internal/user/repository/view/model/refresh_token.go
Normal file
154
internal/user/repository/view/model/refresh_token.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/logging"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
|
user_repo "github.com/caos/zitadel/internal/repository/user"
|
||||||
|
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RefreshTokenKeyTokenID = "id"
|
||||||
|
RefreshTokenKeyUserID = "user_id"
|
||||||
|
RefreshTokenKeyApplicationID = "application_id"
|
||||||
|
RefreshTokenKeyUserAgentID = "user_agent_id"
|
||||||
|
RefreshTokenKeyExpiration = "expiration"
|
||||||
|
RefreshTokenKeyResourceOwner = "resource_owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshTokenView struct {
|
||||||
|
ID string `json:"tokenId" gorm:"column:id"`
|
||||||
|
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||||
|
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||||
|
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||||
|
Token string `json:"-" gorm:"column:token"`
|
||||||
|
UserID string `json:"-" gorm:"column:user_id;primary_key"`
|
||||||
|
ClientID string `json:"clientID" gorm:"column:client_id;primary_key"`
|
||||||
|
UserAgentID string `json:"userAgentId" gorm:"column:user_agent_id;primary_key"`
|
||||||
|
Audience pq.StringArray `json:"audience" gorm:"column:audience"`
|
||||||
|
Scopes pq.StringArray `json:"scopes" gorm:"column:scopes"`
|
||||||
|
AuthMethodsReferences pq.StringArray `json:"authMethodsReference" gorm:"column:amr"`
|
||||||
|
AuthTime time.Time `json:"authTime" gorm:"column:auth_time"`
|
||||||
|
IdleExpiration time.Time `json:"-" gorm:"column:idle_expiration"`
|
||||||
|
Expiration time.Time `json:"-" gorm:"column:expiration"`
|
||||||
|
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshTokenViewsToModel(tokens []*RefreshTokenView) []*usr_model.RefreshTokenView {
|
||||||
|
result := make([]*usr_model.RefreshTokenView, len(tokens))
|
||||||
|
for i, g := range tokens {
|
||||||
|
result[i] = RefreshTokenViewToModel(g)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshTokenViewToModel(token *RefreshTokenView) *usr_model.RefreshTokenView {
|
||||||
|
return &usr_model.RefreshTokenView{
|
||||||
|
ID: token.ID,
|
||||||
|
CreationDate: token.CreationDate,
|
||||||
|
ChangeDate: token.ChangeDate,
|
||||||
|
ResourceOwner: token.ResourceOwner,
|
||||||
|
Token: token.Token,
|
||||||
|
UserID: token.UserID,
|
||||||
|
ClientID: token.ClientID,
|
||||||
|
UserAgentID: token.UserAgentID,
|
||||||
|
Audience: token.Audience,
|
||||||
|
Scopes: token.Scopes,
|
||||||
|
AuthMethodsReferences: token.AuthMethodsReferences,
|
||||||
|
AuthTime: token.AuthTime,
|
||||||
|
IdleExpiration: token.IdleExpiration,
|
||||||
|
Expiration: token.Expiration,
|
||||||
|
Sequence: token.Sequence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshTokenView) AppendEventIfMyRefreshToken(event *es_models.Event) (err error) {
|
||||||
|
view := new(RefreshTokenView)
|
||||||
|
switch eventstore.EventType(event.Type) {
|
||||||
|
case user_repo.HumanRefreshTokenAddedType:
|
||||||
|
view.setRootData(event)
|
||||||
|
err = view.appendAddedEvent(event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case user_repo.HumanRefreshTokenRenewedType:
|
||||||
|
view.setRootData(event)
|
||||||
|
err = view.appendRenewedEvent(event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case user_repo.HumanRefreshTokenRemovedType,
|
||||||
|
user_repo.UserRemovedType,
|
||||||
|
user_repo.UserDeactivatedType,
|
||||||
|
user_repo.UserLockedType:
|
||||||
|
view.appendRemovedEvent(event)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if view.ID == t.ID {
|
||||||
|
return t.AppendEvent(event)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshTokenView) AppendEvent(event *es_models.Event) error {
|
||||||
|
t.ChangeDate = event.CreationDate
|
||||||
|
t.Sequence = event.Sequence
|
||||||
|
switch eventstore.EventType(event.Type) {
|
||||||
|
case user_repo.HumanRefreshTokenAddedType:
|
||||||
|
t.setRootData(event)
|
||||||
|
return t.appendAddedEvent(event)
|
||||||
|
case user_repo.HumanRefreshTokenRenewedType:
|
||||||
|
t.setRootData(event)
|
||||||
|
return t.appendRenewedEvent(event)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshTokenView) setRootData(event *es_models.Event) {
|
||||||
|
t.UserID = event.AggregateID
|
||||||
|
t.ResourceOwner = event.ResourceOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshTokenView) appendAddedEvent(event *es_models.Event) error {
|
||||||
|
e := new(user_repo.HumanRefreshTokenAddedEvent)
|
||||||
|
if err := json.Unmarshal(event.Data, e); err != nil {
|
||||||
|
logging.Log("EVEN-Dbb31").WithError(err).Error("could not unmarshal event data")
|
||||||
|
return caos_errs.ThrowInternal(err, "MODEL-Bbr42", "could not unmarshal event")
|
||||||
|
}
|
||||||
|
t.ID = e.TokenID
|
||||||
|
t.CreationDate = event.CreationDate
|
||||||
|
t.AuthMethodsReferences = e.AuthMethodsReferences
|
||||||
|
t.AuthTime = e.AuthTime
|
||||||
|
t.Audience = e.Audience
|
||||||
|
t.ClientID = e.ClientID
|
||||||
|
t.Expiration = event.CreationDate.Add(e.Expiration)
|
||||||
|
t.IdleExpiration = event.CreationDate.Add(e.IdleExpiration)
|
||||||
|
t.Scopes = e.Scopes
|
||||||
|
t.Token = e.TokenID
|
||||||
|
t.UserAgentID = e.UserAgentID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshTokenView) appendRenewedEvent(event *es_models.Event) error {
|
||||||
|
e := new(user_repo.HumanRefreshTokenRenewedEvent)
|
||||||
|
if err := json.Unmarshal(event.Data, e); err != nil {
|
||||||
|
logging.Log("EVEN-Vbbn2").WithError(err).Error("could not unmarshal event data")
|
||||||
|
return caos_errs.ThrowInternal(err, "MODEL-Bbrn4", "could not unmarshal event")
|
||||||
|
}
|
||||||
|
t.ID = e.TokenID
|
||||||
|
t.IdleExpiration = event.CreationDate.Add(e.IdleExpiration)
|
||||||
|
t.Token = e.RefreshToken
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RefreshTokenView) appendRemovedEvent(event *es_models.Event) {
|
||||||
|
t.Expiration = event.CreationDate
|
||||||
|
}
|
69
internal/user/repository/view/model/refresh_token_query.go
Normal file
69
internal/user/repository/view/model/refresh_token_query.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
|
"github.com/caos/zitadel/internal/view/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshTokenSearchRequest model.RefreshTokenSearchRequest
|
||||||
|
type RefreshTokenSearchQuery model.RefreshTokenSearchQuery
|
||||||
|
type RefreshTokenSearchKey model.RefreshTokenSearchKey
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchRequest) GetLimit() uint64 {
|
||||||
|
return req.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchRequest) GetOffset() uint64 {
|
||||||
|
return req.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||||
|
if req.SortingColumn == model.RefreshTokenSearchKeyUnspecified {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return RefreshTokenSearchKey(req.SortingColumn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchRequest) GetAsc() bool {
|
||||||
|
return req.Asc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchRequest) GetQueries() []repository.SearchQuery {
|
||||||
|
result := make([]repository.SearchQuery, len(req.Queries))
|
||||||
|
for i, q := range req.Queries {
|
||||||
|
result[i] = RefreshTokenSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchQuery) GetKey() repository.ColumnKey {
|
||||||
|
return RefreshTokenSearchKey(req.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchQuery) GetMethod() domain.SearchMethod {
|
||||||
|
return req.Method
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req RefreshTokenSearchQuery) GetValue() interface{} {
|
||||||
|
return req.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key RefreshTokenSearchKey) ToColumnName() string {
|
||||||
|
switch model.RefreshTokenSearchKey(key) {
|
||||||
|
case model.RefreshTokenSearchKeyRefreshTokenID:
|
||||||
|
return RefreshTokenKeyTokenID
|
||||||
|
case model.RefreshTokenSearchKeyUserAgentID:
|
||||||
|
return RefreshTokenKeyUserAgentID
|
||||||
|
case model.RefreshTokenSearchKeyUserID:
|
||||||
|
return RefreshTokenKeyUserID
|
||||||
|
case model.RefreshTokenSearchKeyApplicationID:
|
||||||
|
return RefreshTokenKeyApplicationID
|
||||||
|
case model.RefreshTokenSearchKeyExpiration:
|
||||||
|
return RefreshTokenKeyExpiration
|
||||||
|
case model.RefreshTokenSearchKeyResourceOwner:
|
||||||
|
return RefreshTokenKeyResourceOwner
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
79
internal/user/repository/view/refresh_token_view.go
Normal file
79
internal/user/repository/view/refresh_token_view.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
|
usr_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||||
|
"github.com/caos/zitadel/internal/view/repository"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RefreshTokenByID(db *gorm.DB, table, tokenID string) (*usr_model.RefreshTokenView, error) {
|
||||||
|
token := new(usr_model.RefreshTokenView)
|
||||||
|
query := repository.PrepareGetByKey(table, usr_model.RefreshTokenSearchKey(model.RefreshTokenSearchKeyRefreshTokenID), tokenID)
|
||||||
|
err := query(db, token)
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return nil, errors.ThrowNotFound(nil, "VIEW-6ub3p", "Errors.RefreshToken.NotFound")
|
||||||
|
}
|
||||||
|
return token, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshTokensByUserID(db *gorm.DB, table, userID string) ([]*usr_model.RefreshTokenView, error) {
|
||||||
|
tokens := make([]*usr_model.RefreshTokenView, 0)
|
||||||
|
userIDQuery := &model.RefreshTokenSearchQuery{
|
||||||
|
Key: model.RefreshTokenSearchKeyUserID,
|
||||||
|
Method: domain.SearchMethodEquals,
|
||||||
|
Value: userID,
|
||||||
|
}
|
||||||
|
query := repository.PrepareSearchQuery(table, usr_model.RefreshTokenSearchRequest{
|
||||||
|
Queries: []*model.RefreshTokenSearchQuery{userIDQuery},
|
||||||
|
})
|
||||||
|
_, err := query(db, &tokens)
|
||||||
|
return tokens, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutRefreshToken(db *gorm.DB, table string, token *usr_model.RefreshTokenView) error {
|
||||||
|
save := repository.PrepareSave(table)
|
||||||
|
return save(db, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutRefreshTokens(db *gorm.DB, table string, tokens ...*usr_model.RefreshTokenView) error {
|
||||||
|
save := repository.PrepareBulkSave(table)
|
||||||
|
t := make([]interface{}, len(tokens))
|
||||||
|
for i, token := range tokens {
|
||||||
|
t[i] = token
|
||||||
|
}
|
||||||
|
return save(db, t...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchRefreshTokens(db *gorm.DB, table string, req *model.RefreshTokenSearchRequest) ([]*usr_model.RefreshTokenView, uint64, error) {
|
||||||
|
tokens := make([]*usr_model.RefreshTokenView, 0)
|
||||||
|
query := repository.PrepareSearchQuery(table, usr_model.RefreshTokenSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
|
||||||
|
count, err := query(db, &tokens)
|
||||||
|
return tokens, count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRefreshToken(db *gorm.DB, table, tokenID string) error {
|
||||||
|
delete := repository.PrepareDeleteByKey(table, usr_model.RefreshTokenSearchKey(model.RefreshTokenSearchKeyRefreshTokenID), tokenID)
|
||||||
|
return delete(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSessionRefreshTokens(db *gorm.DB, table, agentID, userID string) error {
|
||||||
|
delete := repository.PrepareDeleteByKeys(table,
|
||||||
|
repository.Key{Key: usr_model.RefreshTokenSearchKey(model.RefreshTokenSearchKeyUserAgentID), Value: agentID},
|
||||||
|
repository.Key{Key: usr_model.RefreshTokenSearchKey(model.RefreshTokenSearchKeyUserID), Value: userID},
|
||||||
|
)
|
||||||
|
return delete(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteUserRefreshTokens(db *gorm.DB, table, userID string) error {
|
||||||
|
delete := repository.PrepareDeleteByKey(table, usr_model.RefreshTokenSearchKey(model.RefreshTokenSearchKeyUserID), userID)
|
||||||
|
return delete(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteApplicationRefreshTokens(db *gorm.DB, table string, appIDs []string) error {
|
||||||
|
delete := repository.PrepareDeleteByKey(table, usr_model.RefreshTokenSearchKey(model.RefreshTokenSearchKeyApplicationID), pq.StringArray(appIDs))
|
||||||
|
return delete(db)
|
||||||
|
}
|
21
migrations/cockroach/V1.43__refresh_tokens.sql
Normal file
21
migrations/cockroach/V1.43__refresh_tokens.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE auth.refresh_tokens (
|
||||||
|
id TEXT,
|
||||||
|
|
||||||
|
creation_date TIMESTAMPTZ,
|
||||||
|
change_date TIMESTAMPTZ,
|
||||||
|
|
||||||
|
resource_owner TEXT,
|
||||||
|
token TEXT,
|
||||||
|
client_id TEXT,
|
||||||
|
user_agent_id TEXT,
|
||||||
|
user_id TEXT,
|
||||||
|
auth_time TIMESTAMPTZ,
|
||||||
|
idle_expiration TIMESTAMPTZ,
|
||||||
|
expiration TIMESTAMPTZ,
|
||||||
|
sequence BIGINT,
|
||||||
|
scopes TEXT ARRAY,
|
||||||
|
audience TEXT ARRAY,
|
||||||
|
amr TEXT ARRAY,
|
||||||
|
|
||||||
|
PRIMARY KEY (client_id, user_agent_id, user_id)
|
||||||
|
);
|
@ -82,6 +82,39 @@ service AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the refresh tokens of the authorized user
|
||||||
|
rpc ListMyRefreshTokens(ListMyRefreshTokensRequest) returns (ListMyRefreshTokensResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/users/me/tokens/refresh/_search"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "authenticated"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revokes a single refresh token of the authorized user by its (token) id
|
||||||
|
rpc RevokeMyRefreshToken(RevokeMyRefreshTokenRequest) returns (RevokeMyRefreshTokenResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/users/me/tokens/refresh/{id}"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "authenticated"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revokes all refresh tokens of the authorized user
|
||||||
|
rpc RevokeAllMyRefreshTokens(RevokeAllMyRefreshTokensRequest) returns (RevokeAllMyRefreshTokensResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/users/me/tokens/refresh/_revoke_all"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "authenticated"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Change the user name of the authorize user
|
// Change the user name of the authorize user
|
||||||
rpc UpdateMyUserName(UpdateMyUserNameRequest) returns (UpdateMyUserNameResponse) {
|
rpc UpdateMyUserName(UpdateMyUserNameRequest) returns (UpdateMyUserNameResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
@ -489,6 +522,28 @@ message ListMyUserSessionsResponse {
|
|||||||
repeated zitadel.user.v1.Session result = 1;
|
repeated zitadel.user.v1.Session result = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//This is an empty request
|
||||||
|
message ListMyRefreshTokensRequest {}
|
||||||
|
|
||||||
|
message ListMyRefreshTokensResponse {
|
||||||
|
zitadel.v1.ListDetails details = 1;
|
||||||
|
repeated zitadel.user.v1.RefreshToken result = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RevokeMyRefreshTokenRequest {
|
||||||
|
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
}
|
||||||
|
|
||||||
|
message RevokeMyRefreshTokenResponse {
|
||||||
|
zitadel.v1.ObjectDetails details = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//This is an empty request
|
||||||
|
message RevokeAllMyRefreshTokensRequest {}
|
||||||
|
|
||||||
|
//This is an empty response
|
||||||
|
message RevokeAllMyRefreshTokensResponse {}
|
||||||
|
|
||||||
message UpdateMyUserNameRequest {
|
message UpdateMyUserNameRequest {
|
||||||
string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
import "zitadel/object.proto";
|
import "zitadel/object.proto";
|
||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
|
||||||
@ -516,6 +517,48 @@ enum SessionState {
|
|||||||
SESSION_STATE_TERMINATED = 2;
|
SESSION_STATE_TERMINATED = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RefreshToken {
|
||||||
|
string id = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629023906489455\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
zitadel.v1.ObjectDetails details = 2;
|
||||||
|
string client_id = 3 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629023906488334@ZITADEL\"";
|
||||||
|
description: "oauth2/oidc client_id of the authorized application";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
google.protobuf.Timestamp auth_time = 4 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "\"time when the user authenticated, does not have to be the same time the token was created\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
google.protobuf.Timestamp idle_expiration = 5 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "\"time the refresh token will expire if not used, the user will have to reauthenticate\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
google.protobuf.Timestamp expiration = 6 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "\"time the refresh token will expire, the user will have to reauthenticate\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
repeated string scopes = 7 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "[\"openid\",\"email\",\"profile\",\"offline_access\"]";
|
||||||
|
description: "scopes of the initial auth request, access tokens created by this refresh token can have a subset of these scopes";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
repeated string audience = 8 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "[\"69629023906488334@ZITADEL\", \"69629023906481256\"]"
|
||||||
|
description: "audience of the initial auth request and of all access tokens created by this refresh token";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
message UserGrant {
|
message UserGrant {
|
||||||
string id = 1 [
|
string id = 1 [
|
||||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user