mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +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:
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user