feat(api): allow Device Authorization Grant using custom login UI (#9387)

# Which Problems Are Solved

The OAuth2 Device Authorization Grant could not yet been handled through
the new login UI, resp. using the session API.
This PR adds the ability for the login UI to get the required
information to display the user and handle their decision (approve with
authorization or deny) using the OIDC Service API.

# How the Problems Are Solved

- Added a `GetDeviceAuthorizationRequest` endpoint, which allows getting
the `id`, `client_id`, `scope`, `app_name` and `project_name` of the
device authorization request
- Added a `AuthorizeOrDenyDeviceAuthorization` endpoint, which allows to
approve/authorize with the session information or deny the request. The
identification of the request is done by the `device_authorization_id` /
`id` returned in the previous request.
- To prevent leaking the `device_code` to the UI, but still having an
easy reference, it's encrypted and returned as `id`, resp. decrypted
when used.
- Fixed returned error types for device token responses on token
endpoint:
- Explicitly return `access_denied` (without internal error) when user
denied the request
  - Default to `invalid_grant` instead of `access_denied`
- Explicitly check on initial state when approving the reqeust
- Properly handle done case (also relates to initial check) 
- Documented the flow and handling in custom UIs (according to OIDC /
SAML)

# Additional Changes

- fixed some typos and punctuation in the corresponding OIDC / SAML
guides.
- added some missing translations for auth and saml request

# Additional Context

- closes #6239

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
Livio Spring
2025-02-25 07:33:13 +01:00
committed by GitHub
parent f2e82d57ac
commit 911200aa9b
39 changed files with 1210 additions and 35 deletions

View File

@@ -13,6 +13,7 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
@@ -636,6 +637,230 @@ func TestServer_CreateCallback_Permission(t *testing.T) {
}
}
func TestServer_GetDeviceAuthorizationRequest(t *testing.T) {
project, err := Instance.CreateProject(CTX)
require.NoError(t, err)
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
require.NoError(t, err)
tests := []struct {
name string
dep func() (*oidc.DeviceAuthorizationResponse, error)
ctx context.Context
want *oidc.DeviceAuthorizationResponse
wantErr bool
}{
{
name: "Not found",
dep: func() (*oidc.DeviceAuthorizationResponse, error) {
return &oidc.DeviceAuthorizationResponse{
UserCode: "notFound",
}, nil
},
ctx: CTX,
wantErr: true,
},
{
name: "success",
dep: func() (*oidc.DeviceAuthorizationResponse, error) {
return Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
},
ctx: CTX,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deviceAuth, err := tt.dep()
require.NoError(t, err)
got, err := Client.GetDeviceAuthorizationRequest(tt.ctx, &oidc_pb.GetDeviceAuthorizationRequestRequest{
UserCode: deviceAuth.UserCode,
})
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
authRequest := got.GetDeviceAuthorizationRequest()
assert.NotNil(t, authRequest)
assert.NotEmpty(t, authRequest.GetId())
assert.Equal(t, client.GetClientId(), authRequest.GetClientId())
assert.Contains(t, authRequest.GetScope(), "openid")
assert.NotEmpty(t, authRequest.GetAppName())
assert.NotEmpty(t, authRequest.GetProjectName())
})
}
}
func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) {
project, err := Instance.CreateProject(CTX)
require.NoError(t, err)
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
require.NoError(t, err)
sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
tests := []struct {
name string
ctx context.Context
req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest
AuthError string
want *oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse
wantURL *url.URL
wantErr bool
}{
{
name: "Not found",
ctx: CTX,
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: "123",
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
wantErr: true,
},
{
name: "session not found",
ctx: CTX,
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: func() string {
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
require.NoError(t, err)
var id string
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
UserCode: req.UserCode,
})
assert.NoError(t, err)
id = resp.GetDeviceAuthorizationRequest().GetId()
}, 5*time.Second, 100*time.Millisecond)
return id
}(),
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
Session: &oidc_pb.Session{
SessionId: "foo",
SessionToken: "bar",
},
},
},
wantErr: true,
},
{
name: "session token invalid",
ctx: CTX,
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: func() string {
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
require.NoError(t, err)
var id string
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
UserCode: req.UserCode,
})
assert.NoError(collectT, err)
id = resp.GetDeviceAuthorizationRequest().GetId()
}, 5*time.Second, 100*time.Millisecond)
return id
}(),
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: "bar",
},
},
},
wantErr: true,
},
{
name: "deny device authorization",
ctx: CTX,
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: func() string {
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
require.NoError(t, err)
var id string
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
UserCode: req.UserCode,
})
assert.NoError(collectT, err)
id = resp.GetDeviceAuthorizationRequest().GetId()
}, 5*time.Second, 100*time.Millisecond)
return id
}(),
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{},
},
want: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{},
wantErr: false,
},
{
name: "authorize, no permission, error",
ctx: CTX,
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: func() string {
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
require.NoError(t, err)
var id string
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
UserCode: req.UserCode,
})
assert.NoError(collectT, err)
id = resp.GetDeviceAuthorizationRequest().GetId()
}, 5*time.Second, 100*time.Millisecond)
return id
}(),
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
wantErr: true,
},
{
name: "authorize, with permission",
ctx: CTXLoginClient,
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: func() string {
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
require.NoError(t, err)
var id string
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
UserCode: req.UserCode,
})
assert.NoError(collectT, err)
id = resp.GetDeviceAuthorizationRequest().GetId()
}, 5*time.Second, 100*time.Millisecond)
return id
}(),
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Client.AuthorizeOrDenyDeviceAuthorization(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func createSession(t *testing.T, ctx context.Context, userID string) *session.CreateSessionResponse {
sessionResp, err := Instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{

View File

@@ -2,6 +2,7 @@ package oidc
import (
"context"
"encoding/base64"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/op"
@@ -28,6 +29,54 @@ func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequest
}, nil
}
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
switch v := req.GetCallbackKind().(type) {
case *oidc_pb.CreateCallbackRequest_Error:
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
case *oidc_pb.CreateCallbackRequest_Session:
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
}
}
func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *oidc_pb.GetDeviceAuthorizationRequestRequest) (*oidc_pb.GetDeviceAuthorizationRequestResponse, error) {
deviceRequest, err := s.query.DeviceAuthRequestByUserCode(ctx, req.GetUserCode())
if err != nil {
return nil, err
}
encrypted, err := s.encryption.Encrypt([]byte(deviceRequest.DeviceCode))
if err != nil {
return nil, err
}
return &oidc_pb.GetDeviceAuthorizationRequestResponse{
DeviceAuthorizationRequest: &oidc_pb.DeviceAuthorizationRequest{
Id: base64.RawURLEncoding.EncodeToString(encrypted),
ClientId: deviceRequest.ClientID,
Scope: deviceRequest.Scopes,
AppName: deviceRequest.AppName,
ProjectName: deviceRequest.ProjectName,
},
}, nil
}
func (s *Server) AuthorizeOrDenyDeviceAuthorization(ctx context.Context, req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest) (*oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse, error) {
deviceCode, err := s.deviceCodeFromID(req.GetDeviceAuthorizationId())
if err != nil {
return nil, err
}
switch req.GetDecision().(type) {
case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session:
_, err = s.command.ApproveDeviceAuthWithSession(ctx, deviceCode, req.GetSession().GetSessionId(), req.GetSession().GetSessionToken())
case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny:
_, err = s.command.CancelDeviceAuth(ctx, deviceCode, domain.DeviceAuthCanceledDenied)
}
if err != nil {
return nil, err
}
return &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{}, nil
}
func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest {
pba := &oidc_pb.AuthRequest{
Id: a.ID,
@@ -87,17 +136,6 @@ func (s *Server) checkPermission(ctx context.Context, clientID string, userID st
return nil
}
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
switch v := req.GetCallbackKind().(type) {
case *oidc_pb.CreateCallbackRequest_Error:
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
case *oidc_pb.CreateCallbackRequest_Session:
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
}
}
func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) {
details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError()))
if err != nil {
@@ -215,3 +253,11 @@ func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
return "server_error"
}
}
func (s *Server) deviceCodeFromID(deviceAuthID string) (string, error) {
decoded, err := base64.RawURLEncoding.DecodeString(deviceAuthID)
if err != nil {
return "", err
}
return s.encryption.DecryptString(decoded, s.encryption.EncryptionKeyID())
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/query"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
)
@@ -20,6 +21,7 @@ type Server struct {
op *oidc.Server
externalSecure bool
encryption crypto.EncryptionAlgorithm
}
type Config struct{}
@@ -29,12 +31,14 @@ func CreateServer(
query *query.Queries,
op *oidc.Server,
externalSecure bool,
encryption crypto.EncryptionAlgorithm,
) *Server {
return &Server{
command: command,
query: query,
op: op,
externalSecure: externalSecure,
encryption: encryption,
}
}

View File

@@ -0,0 +1,127 @@
//go:build integration
package oidc_test
import (
"context"
"slices"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/app"
"github.com/zitadel/zitadel/pkg/grpc/auth"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
)
func TestServer_DeviceAuth(t *testing.T) {
project, err := Instance.CreateProject(CTX)
require.NoError(t, err)
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
require.NoError(t, err)
tests := []struct {
name string
scope []string
decision func(t *testing.T, id string)
wantErr error
}{
{
name: "authorized",
scope: []string{},
decision: func(t *testing.T, id string) {
sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: id,
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
},
},
{
name: "authorized, with ZITADEL",
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL},
decision: func(t *testing.T, id string) {
sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: id,
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
},
},
{
name: "denied",
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL},
decision: func(t *testing.T, id string) {
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
DeviceAuthorizationId: id,
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{
Deny: &oidc_pb.Deny{},
},
})
require.NoError(t, err)
},
wantErr: oidc.ErrAccessDenied(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), client.GetClientId(), "", "", tt.scope)
require.NoError(t, err)
deviceAuthorization, err := rp.DeviceAuthorization(CTX, tt.scope, provider, nil)
require.NoError(t, err)
relyingPartyDone := make(chan struct{})
go func() {
ctx, cancel := context.WithTimeout(CTX, 1*time.Minute)
defer func() {
cancel()
relyingPartyDone <- struct{}{}
}()
tokens, err := rp.DeviceAccessToken(ctx, deviceAuthorization.DeviceCode, time.Duration(deviceAuthorization.Interval)*time.Second, provider)
require.ErrorIs(t, err, tt.wantErr)
if tokens == nil {
return
}
_, err = Instance.Client.Auth.GetMyUser(integration.WithAuthorizationToken(CTX, tokens.AccessToken), &auth.GetMyUserRequest{})
if slices.Contains(tt.scope, domain.ProjectScopeZITADEL) {
require.NoError(t, err)
} else {
require.Error(t, err)
}
}()
var req *oidc_pb.GetDeviceAuthorizationRequestResponse
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
req, err = Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
UserCode: deviceAuthorization.UserCode,
})
assert.NoError(collectT, err)
}, retryDuration, tick)
tt.decision(t, req.GetDeviceAuthorizationRequest().GetId())
<-relyingPartyDone
})
}
}

View File

@@ -42,6 +42,9 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic
if state == domain.DeviceAuthStateExpired {
return nil, oidc.ErrExpiredDeviceCode()
}
if state == domain.DeviceAuthStateDenied {
return nil, oidc.ErrAccessDenied()
}
}
return nil, oidc.ErrAccessDenied().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
return nil, oidc.ErrInvalidGrant().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
}

View File

@@ -59,6 +59,9 @@ func (c *Commands) ApproveDeviceAuth(
if !model.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound")
}
if model.State != domain.DeviceAuthStateInitiated {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-GEJL3", "Errors.DeviceAuth.AlreadyHandled")
}
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID))
if err != nil {
return nil, err
@@ -71,6 +74,60 @@ func (c *Commands) ApproveDeviceAuth(
return writeModelToObjectDetails(&model.WriteModel), nil
}
func (c *Commands) ApproveDeviceAuthWithSession(
ctx context.Context,
deviceCode,
sessionID,
sessionToken string,
) (*domain.ObjectDetails, error) {
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode)
if err != nil {
return nil, err
}
if !model.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound")
}
if model.State != domain.DeviceAuthStateInitiated {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled")
}
if err := c.checkPermission(ctx, domain.PermissionSessionLink, model.ResourceOwner, ""); err != nil {
return nil, err
}
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
if err != nil {
return nil, err
}
if err = sessionWriteModel.CheckIsActive(); err != nil {
return nil, err
}
if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(
ctx,
model.aggregate,
sessionWriteModel.UserID,
sessionWriteModel.UserResourceOwner,
sessionWriteModel.AuthMethodTypes(),
sessionWriteModel.AuthenticationTime(),
sessionWriteModel.PreferredLanguage,
sessionWriteModel.UserAgent,
sessionID,
))
if err != nil {
return nil, err
}
err = AppendAndReduce(model, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&model.WriteModel), nil
}
func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) {
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, id)
if err != nil {

View File

@@ -82,6 +82,7 @@ func (m *DeviceAuthWriteModel) Query() *eventstore.SearchQueryBuilder {
deviceauth.AddedEventType,
deviceauth.ApprovedEventType,
deviceauth.CanceledEventType,
deviceauth.DoneEventType,
).
Builder()
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/deviceauth"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -265,6 +266,310 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
}
}
func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) {
ctx := authz.WithInstanceID(context.Background(), "instance1")
now := time.Now()
pushErr := errors.New("pushErr")
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
deviceCode string
sessionID string
sessionToken string
}
tests := []struct {
name string
fields fields
args args
wantDetails *domain.ObjectDetails
wantErr error
}{
{
name: "not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx,
"notfound",
"sessionID",
"sessionToken",
},
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound"),
},
{
name: "not initialized, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithInstanceID(
"instance1",
deviceauth.NewAddedEvent(
ctx,
deviceauth.NewAggregate("deviceCode", "instance1"),
"client_id", "deviceCode", "456", now,
[]string{"a", "b", "c"},
[]string{"projectID", "clientID"}, true,
),
),
eventFromEventPusherWithInstanceID(
"instance1",
deviceauth.NewCanceledEvent(
ctx,
deviceauth.NewAggregate("deviceCode", "instance1"),
domain.DeviceAuthCanceledDenied,
)),
),
),
},
args: args{
ctx,
"deviceCode",
"sessionID",
"sessionToken",
},
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled"),
},
{
name: "missing permission, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(eventFromEventPusherWithInstanceID(
"instance1",
deviceauth.NewAddedEvent(
ctx,
deviceauth.NewAggregate("deviceCode", "instance1"),
"client_id", "deviceCode", "456", now,
[]string{"a", "b", "c"},
[]string{"projectID", "clientID"}, true,
),
)),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx,
"deviceCode",
"sessionID",
"sessionToken",
},
wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
{
name: "session not active, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(eventFromEventPusherWithInstanceID(
"instance1",
deviceauth.NewAddedEvent(
ctx,
deviceauth.NewAggregate("deviceCode", "instance1"),
"client_id", "deviceCode", "456", now,
[]string{"a", "b", "c"},
[]string{"projectID", "clientID"}, true,
),
)),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx,
"deviceCode",
"sessionID",
"sessionToken",
},
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
},
{
name: "invalid session token, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(eventFromEventPusherWithInstanceID(
"instance1",
deviceauth.NewAddedEvent(
ctx,
deviceauth.NewAggregate("deviceCode", "instance1"),
"client_id", "deviceCode", "456", now,
[]string{"a", "b", "c"},
[]string{"projectID", "clientID"}, true,
),
)),
expectFilter(eventFromEventPusherWithInstanceID(
"instance1",
session.NewAddedEvent(ctx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
)),
)),
tokenVerifier: newMockTokenVerifierInvalid(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx,
"deviceCode",
"sessionID",
"invalidToken",
},
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
},
{
name: "push error",
fields: fields{
eventstore: expectEventstore(
expectFilter(eventFromEventPusherWithInstanceID(
"instance1",
deviceauth.NewAddedEvent(
ctx,
deviceauth.NewAggregate("deviceCode", "instance1"),
"client_id", "deviceCode", "456", now,
[]string{"a", "b", "c"},
[]string{"projectID", "clientID"}, true,
),
)),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(ctx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
),
),
eventFromEventPusher(
session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "orgID", testNow, &language.Afrikaans),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
2*time.Minute),
),
),
expectPushFailed(pushErr,
deviceauth.NewApprovedEvent(
ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID",
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
testNow, &language.Afrikaans, &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx,
"deviceCode",
"sessionID",
"sessionToken",
},
wantErr: pushErr,
},
{
name: "authorized",
fields: fields{
eventstore: expectEventstore(
expectFilter(eventFromEventPusherWithInstanceID(
"instance1",
deviceauth.NewAddedEvent(
ctx,
deviceauth.NewAggregate("deviceCode", "instance1"),
"client_id", "deviceCode", "456", now,
[]string{"a", "b", "c"},
[]string{"projectID", "clientID"}, true,
),
)),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(ctx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
),
),
eventFromEventPusher(
session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "orgID", testNow, &language.Afrikaans),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
2*time.Minute),
),
),
expectPush(
deviceauth.NewApprovedEvent(
ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID",
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
testNow, &language.Afrikaans, &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx,
"deviceCode",
"sessionID",
"sessionToken",
},
wantDetails: &domain.ObjectDetails{
ResourceOwner: "instance1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
sessionTokenVerifier: tt.fields.tokenVerifier,
checkPermission: tt.fields.checkPermission,
}
gotDetails, err := c.ApproveDeviceAuthWithSession(tt.args.ctx, tt.args.deviceCode, tt.args.sessionID, tt.args.sessionToken)
require.ErrorIs(t, err, tt.wantErr)
assertObjectDetails(t, tt.wantDetails, gotDetails)
})
}
}
func TestCommands_CancelDeviceAuth(t *testing.T) {
ctx := authz.WithInstanceID(context.Background(), "instance1")
now := time.Now()

View File

@@ -62,11 +62,13 @@ func (a *AuthRequestSAML) IsValid() bool {
}
type AuthRequestDevice struct {
ClientID string
DeviceCode string
UserCode string
Scopes []string
Audience []string
ClientID string
DeviceCode string
UserCode string
Scopes []string
Audience []string
AppName string
ProjectName string
}
func (*AuthRequestDevice) Type() AuthRequestType {

View File

@@ -144,7 +144,17 @@ func (m *MockRepository) ExpectPushFailed(err error, expectedCommands []eventsto
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Creator(), commands[i].Creator())
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Type(), commands[i].Type())
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Revision(), commands[i].Revision())
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Payload(), commands[i].Payload())
var expectedPayload []byte
expectedPayload, ok := expectedCommand.Payload().([]byte)
if !ok {
expectedPayload, _ = json.Marshal(expectedCommand.Payload())
}
if string(expectedPayload) == "" {
expectedPayload = []byte("null")
}
gotPayload, _ := json.Marshal(commands[i].Payload())
assert.Equal(m.MockPusher.ctrl.T, expectedPayload, gotPayload)
assert.ElementsMatch(m.MockPusher.ctrl.T, expectedCommand.UniqueConstraints(), commands[i].UniqueConstraints())
}

View File

@@ -466,3 +466,11 @@ func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *man
return machine, name, keyResp.GetKeyDetails(), nil
}
func (i *Instance) CreateDeviceAuthorizationRequest(ctx context.Context, clientID string, scopes ...string) (*oidc.DeviceAuthorizationResponse, error) {
provider, err := i.CreateRelyingParty(ctx, clientID, "", scopes...)
if err != nil {
return nil, err
}
return rp.DeviceAuthorization(ctx, scopes, provider, nil)
}

View File

@@ -86,15 +86,24 @@ var deviceAuthSelectColumns = []string{
DeviceAuthRequestColumnUserCode.identifier(),
DeviceAuthRequestColumnScopes.identifier(),
DeviceAuthRequestColumnAudience.identifier(),
AppColumnName.identifier(),
ProjectColumnName.identifier(),
}
func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) {
return sq.Select(deviceAuthSelectColumns...).From(deviceAuthRequestTable.identifier()).PlaceholderFormat(sq.Dollar),
return sq.Select(deviceAuthSelectColumns...).
From(deviceAuthRequestTable.identifier()).
LeftJoin(join(AppOIDCConfigColumnClientID, DeviceAuthRequestColumnClientID)).
LeftJoin(join(AppColumnID, AppOIDCConfigColumnAppID)).
LeftJoin(join(ProjectColumnID, AppColumnProjectID)).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*domain.AuthRequestDevice, error) {
dst := new(domain.AuthRequestDevice)
var (
scopes database.TextArray[string]
audience database.TextArray[string]
scopes database.TextArray[string]
audience database.TextArray[string]
appName sql.NullString
projectName sql.NullString
)
err := row.Scan(
@@ -103,15 +112,20 @@ func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
&dst.UserCode,
&scopes,
&audience,
&appName,
&projectName,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotExisting")
return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotFound")
}
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal")
}
dst.Scopes = scopes
dst.Audience = audience
dst.AppName = appName.String
dst.ProjectName = projectName.String
return dst, nil
}
}

View File

@@ -24,8 +24,17 @@ const (
` projections.device_auth_requests2.device_code,` +
` projections.device_auth_requests2.user_code,` +
` projections.device_auth_requests2.scopes,` +
` projections.device_auth_requests2.audience` +
` FROM projections.device_auth_requests2`
` projections.device_auth_requests2.audience,` +
` projections.apps7.name,` +
` projections.projects4.name` +
` FROM projections.device_auth_requests2` +
` LEFT JOIN projections.apps7_oidc_configs` +
` ON projections.device_auth_requests2.client_id = projections.apps7_oidc_configs.client_id` +
` AND projections.device_auth_requests2.instance_id = projections.apps7_oidc_configs.instance_id` +
` LEFT JOIN projections.apps7 ON projections.apps7_oidc_configs.app_id = projections.apps7.id` +
` AND projections.apps7_oidc_configs.instance_id = projections.apps7.instance_id` +
` LEFT JOIN projections.projects4 ON projections.apps7.project_id = projections.projects4.id` +
` AND projections.apps7.instance_id = projections.projects4.instance_id`
expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC +
` WHERE projections.device_auth_requests2.instance_id = $1` +
` AND projections.device_auth_requests2.user_code = $2`
@@ -40,13 +49,17 @@ var (
"user-code",
database.TextArray[string]{"a", "b", "c"},
[]string{"projectID", "clientID"},
"appName",
"projectName",
}
expectedDeviceAuth = &domain.AuthRequestDevice{
ClientID: "client-id",
DeviceCode: "device1",
UserCode: "user-code",
Scopes: []string{"a", "b", "c"},
Audience: []string{"projectID", "clientID"},
ClientID: "client-id",
DeviceCode: "device1",
UserCode: "user-code",
Scopes: []string{"a", "b", "c"},
Audience: []string{"projectID", "clientID"},
AppName: "appName",
ProjectName: "projectName",
}
)

View File

@@ -561,6 +561,7 @@ Errors:
AlreadyExists: Auth Request вече съществува
NotExisting: Auth Request не съществува
WrongLoginClient: Auth Request, създаден от друг клиент за влизане
AlreadyHandled: Заявката за удостоверяване вече е обработена
OIDCSession:
RefreshTokenInvalid: Токенът за опресняване е невалиден
Token:
@@ -571,8 +572,12 @@ Errors:
AlreadyExists: SAMLRequest вече съществува
NotExisting: SAMLRequest не съществува
WrongLoginClient: SAMLRequest, създаден от друг клиент за влизане
AlreadyHandled: SAML заявката вече е обработена
SAMLSession:
InvalidClient: SAMLResponse не е издаден за този клиент
DeviceAuth:
NotFound: Заявката за авторизация на устройство не съществува
AlreadyHandled: Заявката за авторизация на устройство вече е обработена
Feature:
NotExisting: Функцията не съществува
TypeNotSupported: Типът функция не се поддържа

View File

@@ -541,6 +541,7 @@ Errors:
AlreadyExists: Požadavek na autentizaci již existuje
NotExisting: Požadavek na autentizaci neexistuje
WrongLoginClient: Požadavek na autentizaci vytvořen jiným klientem přihlášení
AlreadyHandled: Žádost o ověření již byla zpracována
OIDCSession:
RefreshTokenInvalid: Obnovovací token je neplatný
Token:
@@ -551,8 +552,12 @@ Errors:
AlreadyExists: SAMLRequest již existuje
NotExisting: SAMLRequest neexistuje
WrongLoginClient: SAMLRequest vytvořený jiným přihlašovacím klientem
AlreadyHandled: SAML požadavek již byl zpracován
SAMLSession:
InvalidClient: Pro tohoto klienta nebyla vydána odpověď SAMLResponse
DeviceAuth:
NotFound: Žádost o autorizaci zařízení neexistuje
AlreadyHandled: Žádost o autorizaci zařízení již byla zpracována
Feature:
NotExisting: Funkce neexistuje
TypeNotSupported: Typ funkce není podporován

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Auth Request existiert bereits
NotExisting: Auth Request existiert nicht
WrongLoginClient: Auth Request wurde von einem anderen Login-Client erstellt
AlreadyHandled: Auth Request wurde bereits bearbeitet
OIDCSession:
RefreshTokenInvalid: Refresh Token ist ungültig
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest existiert bereits
NotExisting: SAMLRequest existiert nicht
WrongLoginClient: SAMLRequest wurde con einem andere Login-Client erstellt
AlreadyHandled: SAMLRequest wurde bereits bearbeitet
SAMLSession:
InvalidClient: SAMLResponse wurde nicht für diesen Client ausgestellt
DeviceAuth:
NotFound: Die Geräteautorisierungsanforderung existiert nicht
AlreadyHandled: Die Geräteautorisierungsanforderung wurde bereits bearbeitet
Feature:
NotExisting: Feature existiert nicht
TypeNotSupported: Feature Typ wird nicht unterstützt

View File

@@ -544,6 +544,7 @@ Errors:
AlreadyExists: Auth Request already exists
NotExisting: Auth Request does not exist
WrongLoginClient: Auth Request created by other login client
AlreadyHandled: Auth Request has already been handled
OIDCSession:
RefreshTokenInvalid: Refresh Token is invalid
Token:
@@ -554,8 +555,12 @@ Errors:
AlreadyExists: SAMLRequest already exists
NotExisting: SAMLRequest does not exist
WrongLoginClient: SAMLRequest created by other login client
AlreadyHandled: SAMLRequest has already been handled
SAMLSession:
InvalidClient: SAMLResponse was not issued for this client
DeviceAuth:
NotFound: Device Authorization Request does not exist
AlreadyHandled: Device Authorization Request has already been handled
Feature:
NotExisting: Feature does not exist
TypeNotSupported: Feature type is not supported

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Auth Request ya existe
NotExisting: Auth Request no existe
WrongLoginClient: Auth Request creado por otro cliente de inicio de sesión
AlreadyHandled: Auth Request ya ha sido procesada
OIDCSession:
RefreshTokenInvalid: El token de refresco no es válido
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest ya existe
NotExisting: SAMLRequest no existe
WrongLoginClient: SAMLRequest creado por otro cliente de inicio de sesión
AlreadyHandled: SAMLRequest ya ha sido procesada
SAMLSession:
InvalidClient: SAMLResponse no ha sido emitido para este cliente
DeviceAuth:
NotFound: La solicitud de autorización del dispositivo no existe
AlreadyHandled: La solicitud de autorización del dispositivo ya ha sido procesada
Feature:
NotExisting: La característica no existe
TypeNotSupported: El tipo de característica no es compatible

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Auth Request existe déjà
NotExisting: Auth Request n'existe pas
WrongLoginClient: Auth Request créé par un autre client de connexion
AlreadyHandled: Auth Request a déjà été traitée
OIDCSession:
RefreshTokenInvalid: Le jeton de rafraîchissement n'est pas valide
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest existe déjà
NotExisting: SAMLRequest n'existe pas
WrongLoginClient: SAMLRequest créé par un autre client de connexion
AlreadyHandled: SAMLRequest a déjà été traitée
SAMLSession:
InvalidClient: SAMLResponse n'a pas été émise pour ce client
DeviceAuth:
NotFound: La demande d'autorisation de l'appareil n'existe pas
AlreadyHandled: La demande d'autorisation de l'appareil a déjà été traitée
Feature:
NotExisting: La fonctionnalité n'existe pas
TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Az Auth Request már létezik
NotExisting: Az Auth Request nem létezik
WrongLoginClient: Az Auth Requestet egy másik bejelentkezési kliens hozta létre
AlreadyHandled: A hitelesítési kérelem már feldolgozva
OIDCSession:
RefreshTokenInvalid: Az Refresh Token érvénytelen
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: A SAMLRequest már létezik
NotExisting: A SAMLRequest nem létezik
WrongLoginClient: A SAMLRequest egy másik bejelentkezési ügyfél által létrehozott
AlreadyHandled: A SAMLRequest már feldolgozva
SAMLSession:
InvalidClient: SAMLResponse nem lett kiadva ehhez az ügyfélhez
DeviceAuth:
NotFound: Az eszközengedélyezési kérelem nem létezik
AlreadyHandled: Az eszközengedélyezési kérelem már feldolgozva
Feature:
NotExisting: A funkció nem létezik
TypeNotSupported: A funkció típusa nem támogatott

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Permintaan Otentikasi sudah ada
NotExisting: Permintaan Otentikasi tidak ada
WrongLoginClient: Permintaan Otentikasi dibuat oleh klien login lain
AlreadyHandled: Permintaan Otentikasi sudah ditangani
OIDCSession:
RefreshTokenInvalid: Token Penyegaran tidak valid
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest sudah ada
NotExisting: SAMLRequest tidak ada
WrongLoginClient: SAMLRequest dibuat oleh klien login lainnya
AlreadyHandled: SAMLRequest sudah ditangani
SAMLSession:
InvalidClient: SAMLResponse tidak dikeluarkan untuk klien ini
DeviceAuth:
NotFound: Permintaan Otorisasi Perangkat tidak ada
AlreadyHandled: Permintaan Otorisasi Perangkat sudah ditangani
Feature:
NotExisting: Fitur tidak ada
TypeNotSupported: Jenis fitur tidak didukung

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Auth Request esiste già
NotExisting: Auth Request non esiste
WrongLoginClient: Auth Request creato da un altro client di accesso
AlreadyHandled: Auth Request è già stata gestita
OIDCSession:
RefreshTokenInvalid: Refresh Token non è valido
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest esiste già
NotExisting: SAMLRequest non esiste
WrongLoginClient: SAMLRequest creato da un altro client di accesso
AlreadyHandled: SAMLRequest è già stata gestita
SAMLSession:
InvalidClient: SAMLResponse non è stato emesso per questo client
DeviceAuth:
NotFound: La richiesta di autorizzazione del dispositivo non esiste
AlreadyHandled: La richiesta di autorizzazione del dispositivo è già stata gestita
Feature:
NotExisting: La funzionalità non esiste
TypeNotSupported: Il tipo di funzionalità non è supportato

View File

@@ -544,6 +544,7 @@ Errors:
AlreadyExists: AuthRequestはすでに存在する
NotExisting: AuthRequest が存在しません
WrongLoginClient: 他のログインクライアントによって作成された AuthRequest
AlreadyHandled: 認証リクエストは既に処理済みです
OIDCSession:
RefreshTokenInvalid: 無効なリフレッシュトークンです
Token:
@@ -554,8 +555,12 @@ Errors:
AlreadyExists: SAMLリクエストはすでに存在します
NotExisting: SAMLリクエストが存在しません
WrongLoginClient: 他のログイン クライアントによって作成された SAMLRequest
AlreadyHandled: SAMLリクエストは既に処理済みです
SAMLSession:
InvalidClient: このクライアントに対してSAMLResponseは発行されませんでした
DeviceAuth:
NotFound: デバイス認証リクエストが存在しません
AlreadyHandled: デバイス認証リクエストは既に処理済みです
Feature:
NotExisting: 機能が存在しません
TypeNotSupported: 機能タイプはサポートされていません

View File

@@ -544,6 +544,7 @@ Errors:
AlreadyExists: 인증 요청이 이미 존재합니다
NotExisting: 인증 요청이 존재하지 않습니다
WrongLoginClient: 다른 로그인 클라이언트에 의해 생성된 인증 요청
AlreadyHandled: 인증 요청이 이미 처리되었습니다
OIDCSession:
RefreshTokenInvalid: 새로 고침 토큰이 유효하지 않습니다
Token:
@@ -554,8 +555,12 @@ Errors:
AlreadyExists: SAMLRequest가 이미 존재합니다
NotExisting: SAMLRequest가 존재하지 않습니다
WrongLoginClient: 다른 로그인 클라이언트가 생성한 SAMLRequest
AlreadyHandled: SAML 요청이 이미 처리되었습니다
SAMLSession:
InvalidClient: 이 클라이언트에 대해 SAMLResponse가 발행되지 않았습니다.
DeviceAuth:
NotFound: 장치 인증 요청이 존재하지 않습니다
AlreadyHandled: 장치 인증 요청이 이미 처리되었습니다
Feature:
NotExisting: 기능이 존재하지 않습니다
TypeNotSupported: 기능 유형이 지원되지 않습니다

View File

@@ -542,6 +542,7 @@ Errors:
AlreadyExists: Барањето за автентикација веќе постои
NotExisting: Барањето за автентикација не постои
WrongLoginClient: Барањето за автификација беше креирано од друг клиент за најавување
AlreadyHandled: Барањето за автентикација е веќе обработено
OIDCSession:
RefreshTokenInvalid: Токенот за освежување е неважечки
Token:
@@ -552,8 +553,12 @@ Errors:
AlreadyExists: SAMLRequest веќе постои
NotExisting: SAMLRequest не постои
WrongLoginClient: SAML Барање создадено од друг клиент за најавување
AlreadyHandled: SAML барањето е веќе обработено
SAMLSession:
InvalidClient: SAMLResponse не беше издаден за овој клиент
DeviceAuth:
NotFound: Барањето за авторизација на уредот не постои
AlreadyHandled: Барањето за авторизација на уредот е веќе обработено
Feature:
NotExisting: Функцијата не постои
TypeNotSupported: Типот на функција не е поддржан

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Auth Verzoek bestaat al
NotExisting: Auth Verzoek bestaat niet
WrongLoginClient: Auth Verzoek aangemaakt door andere login client
AlreadyHandled: Authenticatieverzoek is al verwerkt
OIDCSession:
RefreshTokenInvalid: Refresh Token is ongeldig
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest bestaat al
NotExisting: SAMLRequest bestaat niet
WrongLoginClient: SAMLRequest aangemaakt door andere login client
AlreadyHandled: SAML-verzoek is al verwerkt
SAMLSession:
InvalidClient: SAMLResponse is niet uitgegeven voor deze client
DeviceAuth:
NotFound: Apparaatautorisatieverzoek bestaat niet
AlreadyHandled: Apparaatautorisatieverzoek is al verwerkt
Feature:
NotExisting: Functie bestaat niet
TypeNotSupported: Functie type wordt niet ondersteund

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Auth Request już istnieje
NotExisting: Auth Request nie istnieje
WrongLoginClient: Auth Request utworzony przez innego klienta logowania
AlreadyHandled: Żądanie uwierzytelnienia zostało już obsłużone
OIDCSession:
RefreshTokenInvalid: Refresh Token jest nieprawidłowy
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest już istnieje
NotExisting: SAMLRequest nie istnieje
WrongLoginClient: SAMLRequest utworzony przez innego klienta logowania
AlreadyHandled: Żądanie SAML zostało już obsłużone
SAMLSession:
InvalidClient: SAMLResponse nie został wydany dla tego klienta
DeviceAuth:
NotFound: Żądanie autoryzacji urządzenia nie istnieje
AlreadyHandled: Żądanie autoryzacji urządzenia zostało już obsłużone
Feature:
NotExisting: Funkcja nie istnieje
TypeNotSupported: Typ funkcji nie jest obsługiwany

View File

@@ -542,6 +542,7 @@ Errors:
AlreadyExists: A solicitação de autenticação já existe
NotExisting: A solicitação de autenticação não existe
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
AlreadyHandled: O pedido de autenticação já foi processado
OIDCSession:
RefreshTokenInvalid: O Refresh Token é inválido
Token:
@@ -552,8 +553,12 @@ Errors:
AlreadyExists: O SAMLRequest já existe
NotExisting: O SAMLRequest não existe
WrongLoginClient: SAMLRequest criado por outro cliente de login
AlreadyHandled: O pedido SAML já foi processado
SAMLSession:
InvalidClient: O SAMLResponse não foi emitido para este cliente
DeviceAuth:
NotFound: O pedido de autorização do dispositivo não existe
AlreadyHandled: O pedido de autorização do dispositivo já foi processado
Feature:
NotExisting: O recurso não existe
TypeNotSupported: O tipo de recurso não é compatível

View File

@@ -532,6 +532,7 @@ Errors:
AlreadyExists: Запрос на аутентификацию уже существует
NotExisting: Запрос на аутентификацию не существует
WrongLoginClient: Запрос на аутентификацию, созданный другим клиентом входа
AlreadyHandled: Запрос аутентификации уже обработан
OIDCSession:
RefreshTokenInvalid: Маркер обновления недействителен
Token:
@@ -542,8 +543,12 @@ Errors:
AlreadyExists: SAMLRequest уже существует
NotExisting: SAMLRequest не существует
WrongLoginClient: SAMLRequest создан другим клиентом входа
AlreadyHandled: Запрос SAML уже обработан
SAMLSession:
InvalidClient: SAMLResponse не был отправлен для этого клиента
DeviceAuth:
NotFound: Запрос авторизации устройства не существует
AlreadyHandled: Запрос авторизации устройства уже обработан
Feature:
NotExisting: ункция не существует
TypeNotSupported: Тип объекта не поддерживается

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: Autentiseringsbegäran finns redan
NotExisting: Autentiseringsbegäran existerar inte
WrongLoginClient: Autentiseringsbegäran skapad av annan inloggningsklient
AlreadyHandled: Autentiseringsbegäran har redan hanterats
OIDCSession:
RefreshTokenInvalid: Uppdateringstoken är ogiltig
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest finns redan
NotExisting: SAMLRequest finns inte
WrongLoginClient: SAMLRequest skapad av annan inloggningsklient
AlreadyHandled: SAML-begäran har redan hanterats
SAMLSession:
InvalidClient: SAMLResponse utfärdades inte för den här klienten
DeviceAuth:
NotFound: Begäran om enhetsauktorisering finns inte
AlreadyHandled: Begäran om enhetsauktorisering har redan hanterats
Feature:
NotExisting: Funktionen existerar inte
TypeNotSupported: Funktionstypen stöds inte

View File

@@ -543,6 +543,7 @@ Errors:
AlreadyExists: AuthRequest已经存在
NotExisting: AuthRequest不存在
WrongLoginClient: 其他登录客户端创建的AuthRequest
AlreadyHandled: 身份验证请求已被处理
OIDCSession:
RefreshTokenInvalid: Refresh Token 无效
Token:
@@ -553,8 +554,12 @@ Errors:
AlreadyExists: SAMLRequest 已存在
NotExisting: SAMLRequest不存在
WrongLoginClient: 其他登录客户端创建的 SAMLRequest
AlreadyHandled: SAML请求已被处理
SAMLSession:
InvalidClient: 未向该客户端发出 SAMLResponse
DeviceAuth:
NotFound: 设备授权请求不存在
AlreadyHandled: 设备授权请求已被处理
Feature:
NotExisting: 功能不存在
TypeNotSupported: 不支持功能类型