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:
Livio Amstutz 2021-05-20 13:33:35 +02:00 committed by GitHub
parent bc21eeb114
commit ec5020bebc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2732 additions and 55 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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=

View 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
}

View 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,
}
}

View File

@ -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) }()

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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}),
}
}

View 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)
}

View File

@ -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,

View 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)
}

View 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)
}

View File

@ -17,4 +17,5 @@ type Repository interface {
OrgRepository
IAMRepository
FeaturesRepository
RefreshTokenRepository
}

View File

@ -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"

View File

@ -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) {

View 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
}

View 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
}

File diff suppressed because it is too large Load Diff

View 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
}

View File

@ -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).

View 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
}

View File

@ -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"`
}

View File

@ -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

View File

@ -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

View 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
}

View 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
}

View 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
}

View 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 ""
}
}

View 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)
}

View 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)
);

View File

@ -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}];
}

View File

@ -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) = {