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
36 changed files with 2732 additions and 55 deletions

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