mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-06 12:37:38 +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=
|
||||
DefaultAccessTokenLifetime: 12h
|
||||
DefaultIdTokenLifetime: 12h
|
||||
DefaultRefreshTokenIdleExpiration: 720h #30d
|
||||
DefaultRefreshTokenExpiration: 2160h #90d
|
||||
SigningKeyAlgorithm: RS256
|
||||
UserAgentCookieConfig:
|
||||
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
|
||||
|
||||
> **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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -16,7 +16,7 @@ require (
|
||||
github.com/allegro/bigcache v1.2.1
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc
|
||||
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/cockroachdb/cockroach-go/v2 v2.1.0
|
||||
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/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
|
||||
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.14.7/go.mod h1:JiK5RXSOgag66wiSOMEkS+yS4R46Baz6dGwfr60VfvI=
|
||||
github.com/caos/oidc v0.15.0 h1:lSykVX6yfUbWpJPAZ9/ZCuowo95h7AgfgPaC15lzf4Y=
|
||||
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/go.mod h1:2I8oiZb5SMRm/qTLvwpSmdV0M6ex8J/UKyxUGfKaqJo=
|
||||
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/go.mod h1:h/KNeRx7oYU4SpA4SoY7W2/NxDKEEVuwA6j9A27L4OI=
|
||||
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/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
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.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
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/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||
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.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
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/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
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/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)
|
||||
}
|
||||
|
||||
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)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
var userAgentID, applicationID, userOrgID string
|
||||
@ -107,6 +107,37 @@ func grantsToScopes(grants []*grant_model.UserGrantView) []string {
|
||||
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) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
@ -2,7 +2,6 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@ -11,7 +10,9 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
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/user/model"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -255,3 +256,39 @@ func AMRFromMFAType(mfaType domain.MFAType) string {
|
||||
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 {
|
||||
DefaultLoginURL string
|
||||
SigningKeyAlgorithm string
|
||||
DefaultAccessTokenLifetime types.Duration
|
||||
DefaultIdTokenLifetime types.Duration
|
||||
DefaultLoginURL string
|
||||
SigningKeyAlgorithm string
|
||||
DefaultAccessTokenLifetime types.Duration
|
||||
DefaultIdTokenLifetime types.Duration
|
||||
DefaultRefreshTokenIdleExpiration types.Duration
|
||||
DefaultRefreshTokenExpiration types.Duration
|
||||
}
|
||||
|
||||
type EndpointConfig struct {
|
||||
@ -49,13 +51,15 @@ type Endpoint struct {
|
||||
}
|
||||
|
||||
type OPStorage struct {
|
||||
repo repository.Repository
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
defaultLoginURL string
|
||||
defaultAccessTokenLifetime time.Duration
|
||||
defaultIdTokenLifetime time.Duration
|
||||
signingKeyAlgorithm string
|
||||
repo repository.Repository
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
defaultLoginURL string
|
||||
defaultAccessTokenLifetime time.Duration
|
||||
defaultIdTokenLifetime time.Duration
|
||||
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 {
|
||||
@ -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 {
|
||||
return &OPStorage{
|
||||
repo: repo,
|
||||
command: command,
|
||||
query: query,
|
||||
defaultLoginURL: config.DefaultLoginURL,
|
||||
signingKeyAlgorithm: config.SigningKeyAlgorithm,
|
||||
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime.Duration,
|
||||
defaultIdTokenLifetime: config.DefaultIdTokenLifetime.Duration,
|
||||
repo: repo,
|
||||
command: command,
|
||||
query: query,
|
||||
defaultLoginURL: config.DefaultLoginURL,
|
||||
signingKeyAlgorithm: config.SigningKeyAlgorithm,
|
||||
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime.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}),
|
||||
newLabelPolicy(handler{view, bulkLimit, configs.cycleDuration("LabelPolicy"), 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.AuthRequestRepo
|
||||
eventstore.TokenRepo
|
||||
eventstore.RefreshTokenRepo
|
||||
eventstore.KeyRepository
|
||||
eventstore.ApplicationRepo
|
||||
eventstore.UserSessionRepo
|
||||
@ -110,6 +111,12 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
|
||||
View: view,
|
||||
Eventstore: es,
|
||||
},
|
||||
eventstore.RefreshTokenRepo{
|
||||
View: view,
|
||||
Eventstore: es,
|
||||
SearchLimit: conf.SearchLimit,
|
||||
KeyAlgorithm: keyAlgorithm,
|
||||
},
|
||||
eventstore.KeyRepository{
|
||||
View: view,
|
||||
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
|
||||
IAMRepository
|
||||
FeaturesRepository
|
||||
RefreshTokenRepository
|
||||
}
|
||||
|
@ -2,19 +2,20 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/http"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
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"
|
||||
proj_repo "github.com/caos/zitadel/internal/repository/project"
|
||||
usr_repo "github.com/caos/zitadel/internal/repository/user"
|
||||
|
@ -3,12 +3,14 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"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) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-55n8M", "Errors.IDMissing")
|
||||
if orgID == "" || userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Dbge4", "Errors.IDMissing")
|
||||
}
|
||||
|
||||
existingUser, err := c.userWriteModelByID(ctx, userID, orgID)
|
||||
userWriteModel := NewUserWriteModel(userID, orgID)
|
||||
event, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, audience, scopes, lifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isUserStateExists(existingUser.UserState) {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound")
|
||||
_, err = c.eventstore.PushEvents(ctx, event)
|
||||
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)
|
||||
|
||||
preferredLanguage := ""
|
||||
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, orgID)
|
||||
existingHuman, err := c.getHumanWriteModelByID(ctx, userWriteModel.AggregateID, userWriteModel.ResourceOwner)
|
||||
if existingHuman != nil {
|
||||
preferredLanguage = existingHuman.PreferredLanguage.String()
|
||||
}
|
||||
expiration := time.Now().UTC().Add(lifetime)
|
||||
tokenID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
|
||||
_, err = c.eventstore.PushEvents(ctx,
|
||||
user.NewUserTokenAddedEvent(ctx, userAgg, tokenID, clientID, agentID, preferredLanguage, audience, scopes, expiration))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.Token{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: userID,
|
||||
},
|
||||
TokenID: tokenID,
|
||||
UserAgentID: agentID,
|
||||
ApplicationID: clientID,
|
||||
Audience: audience,
|
||||
Scopes: scopes,
|
||||
Expiration: expiration,
|
||||
PreferredLanguage: preferredLanguage,
|
||||
}, nil
|
||||
userAgg := UserAggregateFromWriteModel(&userWriteModel.WriteModel)
|
||||
return user.NewUserTokenAddedEvent(ctx, userAgg, tokenID, clientID, agentID, preferredLanguage, audience, scopes, expiration),
|
||||
&domain.Token{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: userWriteModel.AggregateID,
|
||||
},
|
||||
TokenID: tokenID,
|
||||
UserAgentID: agentID,
|
||||
ApplicationID: clientID,
|
||||
Audience: audience,
|
||||
Scopes: scopes,
|
||||
Expiration: expiration,
|
||||
PreferredLanguage: preferredLanguage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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(HumanPasswordlessTokenCheckSucceededType, HumanPasswordlessCheckSucceededEventMapper).
|
||||
RegisterFilterEventMapper(HumanPasswordlessTokenCheckFailedType, HumanPasswordlessCheckFailedEventMapper).
|
||||
RegisterFilterEventMapper(HumanRefreshTokenAddedType, HumanRefreshTokenAddedEventMapper).
|
||||
RegisterFilterEventMapper(HumanRefreshTokenRenewedType, HumanRefreshTokenRenewedEventEventMapper).
|
||||
RegisterFilterEventMapper(HumanRefreshTokenRemovedType, HumanRefreshTokenRemovedEventEventMapper).
|
||||
RegisterFilterEventMapper(MachineAddedEventType, MachineAddedEventMapper).
|
||||
RegisterFilterEventMapper(MachineChangedEventType, MachineChangedEventMapper).
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||
)
|
||||
@ -202,7 +203,7 @@ type UserTokenAddedEvent struct {
|
||||
ApplicationID string `json:"applicationId"`
|
||||
UserAgentID string `json:"userAgentId"`
|
||||
Audience []string `json:"audience"`
|
||||
Scopes []string `json:"scopes""`
|
||||
Scopes []string `json:"scopes"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
PreferredLanguage string `json:"preferredLanguage"`
|
||||
}
|
||||
|
@ -108,6 +108,9 @@ Errors:
|
||||
BeginLoginFailed: Es ist ein Fehler beim WebAuthN Login aufgetreten
|
||||
ValidateLoginFailed: Zugangsdaten konnten nicht validiert werden
|
||||
CloneWarning: Authentifizierungsdaten wurden möglicherweise geklont
|
||||
RefreshToken:
|
||||
Invalid: Refresh Token ist ungültig
|
||||
NotFound: Refresh Token nicht gefunden
|
||||
Org:
|
||||
AlreadyExist: Organisationsname existiert bereits
|
||||
Invalid: Organisation ist ungültig
|
||||
|
@ -108,6 +108,9 @@ Errors:
|
||||
BeginLoginFailed: WebAuthN begin login failed
|
||||
ValidateLoginFailed: Error on validate login credentials
|
||||
CloneWarning: Credentials may be cloned
|
||||
RefreshToken:
|
||||
Invalid: Refresh Token is invalid
|
||||
NotFound: Refresh Token not found
|
||||
Org:
|
||||
AlreadyExist: Organisationname already taken
|
||||
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
|
||||
rpc UpdateMyUserName(UpdateMyUserNameRequest) returns (UpdateMyUserNameResponse) {
|
||||
option (google.api.http) = {
|
||||
@ -489,6 +522,28 @@ message ListMyUserSessionsResponse {
|
||||
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 {
|
||||
string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ syntax = "proto3";
|
||||
|
||||
import "zitadel/object.proto";
|
||||
import "validate/validate.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
|
||||
@ -516,6 +517,48 @@ enum SessionState {
|
||||
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 {
|
||||
string id = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user