Merge branch 'main' into next-rc

# Conflicts:
#	internal/api/http/middleware/instance_interceptor.go
#	internal/api/http/middleware/instance_interceptor_test.go
This commit is contained in:
Livio Spring
2023-12-07 08:09:06 +01:00
195 changed files with 6020 additions and 2996 deletions

1
.gitignore vendored
View File

@@ -25,6 +25,7 @@ sandbox.go
.idea .idea
.vscode .vscode
.DS_STORE .DS_STORE
.run
# credential # credential
google-credentials google-credentials

View File

@@ -103,7 +103,7 @@ core_unit_test:
core_integration_setup: core_integration_setup:
go build -o zitadel main.go go build -o zitadel main.go
./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml ./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml ./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/zitadel.yaml --steps internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
$(RM) zitadel $(RM) zitadel
.PHONY: core_integration_test .PHONY: core_integration_test

View File

@@ -838,6 +838,11 @@ DefaultInstance:
# DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org # DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org
# If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests. # If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests.
DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION
# AllowedLanguages restricts the languages that can be used.
# If the list is empty, all supported languages are allowed.
AllowedLanguages: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_ALLOWEDLANGUAGES
# - en
# - de
Quotas: Quotas:
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist. # Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
# The following unit types are supported # The following unit types are supported

View File

@@ -1,2 +1,2 @@
-- replace %[1]s with the name of the user -- replace %[1]s with the name of the user
CREATE USER IF NOT EXISTS %[1]s CREATE USER IF NOT EXISTS "%[1]s"

View File

@@ -1,2 +1,2 @@
-- replace %[1]s with the name of the database -- replace %[1]s with the name of the database
CREATE DATABASE IF NOT EXISTS %[1]s CREATE DATABASE IF NOT EXISTS "%[1]s"

View File

@@ -1,4 +1,4 @@
-- replace the first %[1]s with the database -- replace the first %[1]s with the database
-- replace the second \%[2]s with the user -- replace the second \%[2]s with the user
GRANT ALL ON DATABASE %[1]s TO %[2]s; GRANT ALL ON DATABASE "%[1]s" TO "%[2]s";
GRANT SYSTEM VIEWACTIVITY TO %[2]s; GRANT SYSTEM VIEWACTIVITY TO "%[2]s";

View File

@@ -1,3 +1,3 @@
CREATE SCHEMA IF NOT EXISTS eventstore; CREATE SCHEMA IF NOT EXISTS eventstore;
GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO %[1]s; GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s";

View File

@@ -1,3 +1,3 @@
CREATE SCHEMA IF NOT EXISTS projections; CREATE SCHEMA IF NOT EXISTS projections;
GRANT ALL ON ALL TABLES IN SCHEMA projections TO %[1]s; GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s";

View File

@@ -1,3 +1,3 @@
CREATE SCHEMA IF NOT EXISTS system; CREATE SCHEMA IF NOT EXISTS system;
GRANT ALL ON ALL TABLES IN SCHEMA system TO %[1]s; GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s";

View File

@@ -1 +1 @@
CREATE USER %[1]s CREATE USER "%[1]s"

View File

@@ -1 +1 @@
CREATE DATABASE %[1]s CREATE DATABASE "%[1]s"

View File

@@ -1,3 +1,3 @@
-- replace the first %[1]s with the database -- replace the first %[1]s with the database
-- replace the second \%[2]s with the user -- replace the second \%[2]s with the user
GRANT ALL ON DATABASE %[1]s TO %[2]s; GRANT ALL ON DATABASE "%[1]s" TO "%[2]s";

View File

@@ -1,3 +1,3 @@
CREATE SCHEMA IF NOT EXISTS eventstore; CREATE SCHEMA IF NOT EXISTS eventstore;
GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO %[1]s; GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s";

View File

@@ -1,3 +1,3 @@
CREATE SCHEMA IF NOT EXISTS projections; CREATE SCHEMA IF NOT EXISTS projections;
GRANT ALL ON ALL TABLES IN SCHEMA projections TO %[1]s; GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s";

View File

@@ -1,3 +1,3 @@
CREATE SCHEMA IF NOT EXISTS system; CREATE SCHEMA IF NOT EXISTS system;
GRANT ALL ON ALL TABLES IN SCHEMA system TO %[1]s; GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s";

View File

@@ -26,7 +26,7 @@ func Test_verifyDB(t *testing.T) {
name: "doesn't exists, create fails", name: "doesn't exists, create fails",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", sql.ErrTxDone), expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", sql.ErrTxDone),
), ),
database: "zitadel", database: "zitadel",
}, },
@@ -36,7 +36,7 @@ func Test_verifyDB(t *testing.T) {
name: "doesn't exists, create successful", name: "doesn't exists, create successful",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", nil), expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil),
), ),
database: "zitadel", database: "zitadel",
}, },
@@ -46,7 +46,7 @@ func Test_verifyDB(t *testing.T) {
name: "already exists", name: "already exists",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", nil), expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil),
), ),
database: "zitadel", database: "zitadel",
}, },

View File

@@ -21,7 +21,7 @@ func Test_verifyGrant(t *testing.T) {
name: "doesn't exists, create fails", name: "doesn't exists, create fails",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", sql.ErrTxDone), expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", sql.ErrTxDone),
), ),
database: "zitadel", database: "zitadel",
username: "zitadel-user", username: "zitadel-user",
@@ -32,7 +32,7 @@ func Test_verifyGrant(t *testing.T) {
name: "correct", name: "correct",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil), expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil),
), ),
database: "zitadel", database: "zitadel",
username: "zitadel-user", username: "zitadel-user",
@@ -43,7 +43,7 @@ func Test_verifyGrant(t *testing.T) {
name: "already exists", name: "already exists",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil), expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil),
), ),
database: "zitadel", database: "zitadel",
username: "zitadel-user", username: "zitadel-user",

View File

@@ -27,7 +27,7 @@ func Test_verifyUser(t *testing.T) {
name: "doesn't exists, create fails", name: "doesn't exists, create fails",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user", sql.ErrTxDone), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", sql.ErrTxDone),
), ),
username: "zitadel-user", username: "zitadel-user",
password: "", password: "",
@@ -38,7 +38,7 @@ func Test_verifyUser(t *testing.T) {
name: "correct without password", name: "correct without password",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user", nil), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", nil),
), ),
username: "zitadel-user", username: "zitadel-user",
password: "", password: "",
@@ -49,7 +49,7 @@ func Test_verifyUser(t *testing.T) {
name: "correct with password", name: "correct with password",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user WITH PASSWORD 'password'", nil), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil),
), ),
username: "zitadel-user", username: "zitadel-user",
password: "password", password: "password",
@@ -60,7 +60,7 @@ func Test_verifyUser(t *testing.T) {
name: "already exists", name: "already exists",
args: args{ args: args{
db: prepareDB(t, db: prepareDB(t,
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user WITH PASSWORD 'password'", nil), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil),
), ),
username: "zitadel-user", username: "zitadel-user",
password: "", password: "",

View File

@@ -1,3 +1,3 @@
CREATE SCHEMA IF NOT EXISTS logstore; CREATE SCHEMA IF NOT EXISTS logstore;
GRANT ALL ON ALL TABLES IN SCHEMA logstore TO %[1]s; GRANT ALL ON ALL TABLES IN SCHEMA logstore TO "%[1]s";

View File

@@ -16,6 +16,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
new_es "github.com/zitadel/zitadel/internal/eventstore/v3" new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/migration" "github.com/zitadel/zitadel/internal/migration"
"github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/query/projection"
) )
@@ -64,6 +65,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
ctx := context.Background() ctx := context.Background()
logging.Info("setup started") logging.Info("setup started")
i18n.MustLoadSupportedLanguagesFromDir()
zitadelDBClient, err := database.Connect(config.Database, false, false) zitadelDBClient, err := database.Connect(config.Database, false, false)
logging.OnError(err).Fatal("unable to connect to database") logging.OnError(err).Fatal("unable to connect to database")
esPusherDBClient, err := database.Connect(config.Database, false, true) esPusherDBClient, err := database.Connect(config.Database, false, true)

View File

@@ -62,6 +62,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
new_es "github.com/zitadel/zitadel/internal/eventstore/v3" new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/logstore/emitters/access" "github.com/zitadel/zitadel/internal/logstore/emitters/access"
@@ -93,7 +94,6 @@ Requirements:
if err != nil { if err != nil {
return err return err
} }
return startZitadel(config, masterKey, server) return startZitadel(config, masterKey, server)
}, },
} }
@@ -123,6 +123,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
ctx := context.Background() ctx := context.Background()
i18n.MustLoadSupportedLanguagesFromDir()
zitadelDBClient, err := database.Connect(config.Database, false, false) zitadelDBClient, err := database.Connect(config.Database, false, false)
if err != nil { if err != nil {
return fmt.Errorf("cannot start client for projection: %w", err) return fmt.Errorf("cannot start client for projection: %w", err)
@@ -215,6 +217,7 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
if err != nil { if err != nil {
return fmt.Errorf("cannot start commands: %w", err) return fmt.Errorf("cannot start commands: %w", err)
} }
defer commands.Close(ctx) // wait for background jobs
clock := clockpkg.New() clock := clockpkg.New()
actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]()) actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]())

View File

@@ -83,8 +83,8 @@ export class ActionTableComponent implements OnInit {
this.mgmtService this.mgmtService
.deleteAction(action.id) .deleteAction(action.id)
.then(() => { .then(() => {
this.selection.clear();
this.toast.showInfo('FLOWS.DIALOG.DELETEACTION.DELETE_SUCCESS', true); this.toast.showInfo('FLOWS.DIALOG.DELETEACTION.DELETE_SUCCESS', true);
this.refreshPage(); this.refreshPage();
}) })
.catch((error: any) => { .catch((error: any) => {

View File

@@ -309,6 +309,7 @@ export class UserTableComponent implements OnInit {
setTimeout(() => { setTimeout(() => {
this.refreshPage(); this.refreshPage();
}, 1000); }, 1000);
this.selection.clear();
this.toast.showInfo('USER.TOAST.DELETED', true); this.toast.showInfo('USER.TOAST.DELETED', true);
}) })
.catch((error) => { .catch((error) => {

View File

@@ -8,9 +8,26 @@ OAuth 2 Token Introspection.
At the end of the guide you should have an API with a protected endpoint. At the end of the guide you should have an API with a protected endpoint.
> This documentation references our HTTP example. There's also one for GRPC. Check them out on [GitHub](https://github.com/zitadel/zitadel-go/tree/authorization/example/api).
## Set up application and obtain keys
Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console.
You'll need to provide some information about your app. We recommend creating a new app to start from scratch. Navigate to your Project, then add a new application at the top of the page.
Select the **API** application type and continue.
![Create app in console](/img/go/api-create.png)
We recommend that you use JWT Profile for authenticating at the Introspection Endpoint.
![Create app in console](/img/go/api-create-auth.png)
Then create a new key with your desired expiration date. Be sure to download it, as you won't be able to retrieve it again.
![Create api key in console](/img/go/api-create-key.png)
## Prerequisites ## Prerequisites
The client [SDK](https://github.com/zitadel/zitadel-go) will provides an interceptor for both GRPC and HTTP.
This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [OIDC client library](https://github.com/zitadel/oidc). This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [OIDC client library](https://github.com/zitadel/oidc).
All that is required, is to create your API and download the private key file later called `Key JSON` for the service user. All that is required, is to create your API and download the private key file later called `Key JSON` for the service user.
@@ -18,134 +35,170 @@ All that is required, is to create your API and download the private key file la
### Add Go SDK to your project ### Add Go SDK to your project
You need to add the SDK into Go Modules by: You need to add the [SDK](https://github.com/zitadel/zitadel-go) into Go Modules by:
```bash ```bash
go get github.com/zitadel/zitadel-go/v2 go get -u github.com/zitadel/zitadel-go/v3
``` ```
### Create example API ### Create example API
Create a new go file with the content below. This will create an API with two endpoints. On path `/public` it will always write Create a new go file with the content below. This will create an API with three endpoints:
back `ok` and the current timestamp. On `/protected` it will respond the same but only if a valid access_token is sent. The token - `/api/healthz`: can be called by anyone and always returns `OK`
must not be expired and the API has to be part of the audience (either client_id or project_id). - `/api/tasks`: requires authorization and returns the available tasks
- `/api/add-task`: requires authorization with granted `admin` role and adds the task to the list
Make sure to fill the var `issuer` with your own domain. This is the domain of your instance you can find it on the instance detail in the ZITADEL Cloud Customer Portal or in the ZITADEL Console. If authorization is required, the token must not be expired and the API has to be part of the audience (either client_id or project_id).
```go
package main
import ( For tests we will use a Personal Access Token.
"flag"
"log"
"net/http"
"time"
http_mw "github.com/zitadel/zitadel-go/v2/pkg/api/middleware/http"
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
)
var (
issuer = flag.String("issuer", "", "issuer of your ZITADEL instance (in the form: https://<instance>.zitadel.cloud or https://<yourdomain>)")
)
func main() {
flag.Parse()
introspection, err := http_mw.NewIntrospectionInterceptor(*issuer, middleware.OSKeyPath())
if err != nil {
log.Fatal(err)
}
router := http.NewServeMux()
router.HandleFunc("/public", writeOK)
router.HandleFunc("/protected", introspection.HandlerFunc(writeOK))
lis := "127.0.0.1:5001"
log.Fatal(http.ListenAndServe(lis, router))
}
func writeOK(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK " + time.Now().String()))
}
```go reference
https://github.com/zitadel/zitadel-go/blob/next/example/api/http/main.go
``` ```
#### Key JSON You will need to provide some values for the program to run:
- `domain`: Your ZITADEL instance domain, e.g. https://my-domain.zitadel.cloud
- `key`: The path to the downloaded key.json
- `port`: The port on which the API will be accessible, default it 8089
To provide the key JSON to the SDK, simply set an environment variable `ZITADEL_KEY_PATH` with the path to the JSON as value. ## Test API
```bash
export ZITADEL_KEY_PATH=/Users/test/apikey.json
```
For development purposes you should be able to set this in your IDE.
If you're not able to set it via environment variable, you can also exchange the `middleware.OSKeyPath()` and pass it directly:
```go
introspection, err := http_mw.NewIntrospectionInterceptor(
client.Issuer,
"/Users/test/apikey.json",
)
```
### Test API
After you have configured everything correctly, you can simply start the example by: After you have configured everything correctly, you can simply start the example by:
```bash ```bash
go run main.go go run main.go --domain <your domain> --key <path>
``` ```
You can now call the API by browser or curl. Try the public endpoint first: This could look like:
```bash ```bash
curl -i localhost:5001/public go run main.go --domain https://my-domain.zitadel.cloud --key ./api.json
```
After you get a successful log:
```
2023/12/04 10:27:42 INFO server listening, press ctrl+c to stop addr=http://localhost:8089
```
### Public endpoint
Now you can call the API by browser or curl. Try the healthz endpoint first:
```bash
curl -i http://localhost:8089/api/healthz
``` ```
it should return something like: it should return something like:
``` ```
HTTP/1.1 200 OK HTTP/1.1 200 OK
Date: Tue, 24 Aug 2021 11:11:17 GMT Content-Type: application/json
Content-Length: 59 Date: Mon, 04 Dec 2023 09:29:38 GMT
Content-Type: text/plain; charset=utf-8 Content-Length: 4
OK 2021-08-24 13:11:17.135719 +0200 CEST m=+30704.913892168 "OK"
``` ```
and the protected: ### Task list
and the task list endpoint:
```bash ```bash
curl -i localhost:5001/protected curl -i http://localhost:8089/api/tasks
``` ```
it will return: it will return:
``` ```
HTTP/1.1 401 Unauthorized HTTP/1.1 401 Unauthorized
Content-Type: application/json Content-Type: text/plain; charset=utf-8
Date: Tue, 24 Aug 2021 11:13:10 GMT X-Content-Type-Options: nosniff
Content-Length: 21 Date: Mon, 04 Dec 2023 09:41:54 GMT
Content-Length: 44
"auth header missing" unauthorized: authorization header is empty
``` ```
Get a valid access_token for the API. You can achieve this by login into an application of the same project or Get a valid access_token for the API. You can either achieve this by getting an access token with the project_id in the audience
by explicitly requesting the project_id for the audience by scope `urn:zitadel:iam:org:project:id:{projectid}:aud`. or use a PAT of a service account.
If you provide a valid Bearer Token: If you provide a valid Bearer Token:
```bash ```bash
curl -i -H "Authorization: Bearer ${token}" localhost:5001/protected curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks
``` ```
it will return an OK response as well: it will return an empty list:
``` ```
HTTP/1.1 200 OK HTTP/1.1 200 OK
Date: Tue, 24 Aug 2021 11:13:33 GMT Content-Type: application/json
Content-Length: 59 Date: Mon, 04 Dec 2023 09:49:06 GMT
Content-Type: text/plain; charset=utf-8 Content-Length: 2
OK 2021-08-24 13:13:33.131943 +0200 CEST m=+30840.911149251 {}
```
### Try to add a new task
Let's see what happens if you call the AddTask endpoint:
```bash
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task
```
it will complain about the missing `admin` role:
```
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Mon, 04 Dec 2023 09:52:00 GMT
Content-Length: 50
permission denied: missing required role: `admin`
```
### Add admin role
So let's create the role and grant it to the user. To do so, go to your project in ZITADEL Console
and create the role by selecting `Roles` in the navigation and then clicking on the `New Role` button.
Finally, create the role as shown below:
![Create project role in console](/img/go/api-project-role.png)
After you have created the role, let's grant it the user, who requested the tasks.
Click on `Authorization` in the navigation and create a new one by selecting the user and the `admin` role.
After successful creation, it should look like:
![Created authorization in console](/img/go/api-project-auth.png)
So you should now be able to add a new task:
```bash
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task --data "task=My new task"
```
which will report back the successful addition:
```
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 04 Dec 2023 10:06:29 GMT
Content-Length: 26
"task `My new task` added"
```
Let's now retrieve the task list again:
```bash
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks
```
As you can see your new task ist listed. And since you're an `admin` now, you will always get an additional `create a new task on /api/add-task`:
```
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 04 Dec 2023 10:08:38 GMT
Content-Length: 62
{"tasks":["My new task","create a new task on /api/add-task"]}
``` ```

View File

@@ -8,7 +8,11 @@ Users with the role IAM_OWNER can change the restrictions of their instance usin
Currently, the following restrictions are available: Currently, the following restrictions are available:
- *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests. - *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests.
- *[Coming soon](https://github.com/zitadel/zitadel/issues/6250): AllowedLanguages* - *AllowedLanguages* - The following rules apply if languages are restricted:
- Only allowed languages are listed in the OIDC discovery endpoint */.well-kown/openid-configuration*.
- Login UI texts are only rendered in allowed languages.
- Notification message texts are only rendered in allowed languages.
- Custom Texts can be created for disallowed languages as long as ZITADEL supports that language. Therefore, all texts can be customized before allowing a language.
Feature restrictions for an instance are intended to be configured by a user that is managed within that instance. Feature restrictions for an instance are intended to be configured by a user that is managed within that instance.
However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users. However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users.

View File

@@ -12,6 +12,8 @@ Date: Calendar week 41/42 2023
Versions >= 2.39.0 require the cockroach database user of ZITADEL to be granted to the `VIEWACTIVITY` grant. This can either be reached by grant the role manually or execute the `zitadel init` command. Versions >= 2.39.0 require the cockroach database user of ZITADEL to be granted to the `VIEWACTIVITY` grant. This can either be reached by grant the role manually or execute the `zitadel init` command.
Cockroach versions 22.2 < 22.2.11 and 23.1 < 23.1.4 will fail the migration. Please make sure to upgrade to more recent versions first. ZITADEL recommends to use the latest stable version of Cockroachdb.
## Statement ## Statement
To query correct order of events the cockroach database user of ZITADEL needs additional privileges to query the `crdb_internal.cluster_transactions`-table To query correct order of events the cockroach database user of ZITADEL needs additional privileges to query the `crdb_internal.cluster_transactions`-table
@@ -20,6 +22,8 @@ To query correct order of events the cockroach database user of ZITADEL needs ad
Before migrating to versions >= 2.39.0 make sure the cockroach database user has sufficient grants. Before migrating to versions >= 2.39.0 make sure the cockroach database user has sufficient grants.
Cockroachdb version is up to date.
## Impact ## Impact
If the user doesn't have sufficient grants, events won't be updated. If the user doesn't have sufficient grants, events won't be updated.

BIN
docs/static/img/go/api-create-auth.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
docs/static/img/go/api-create-key.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
docs/static/img/go/api-create.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
docs/static/img/go/api-project-auth.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/static/img/go/api-project-role.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

13
go.mod
View File

@@ -13,6 +13,7 @@ require (
github.com/allegro/bigcache v1.2.1 github.com/allegro/bigcache v1.2.1
github.com/benbjohnson/clock v1.3.5 github.com/benbjohnson/clock v1.3.5
github.com/boombuler/barcode v1.0.1 github.com/boombuler/barcode v1.0.1
github.com/brianvoe/gofakeit/v6 v6.25.0
github.com/cockroachdb/cockroach-go/v2 v2.3.5 github.com/cockroachdb/cockroach-go/v2 v2.3.5
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
github.com/crewjam/saml v0.4.14 github.com/crewjam/saml v0.4.14
@@ -49,7 +50,6 @@ require (
github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/gu v0.3.1
github.com/muhlemmer/httpforwarded v0.1.0 github.com/muhlemmer/httpforwarded v0.1.0
github.com/nicksnyder/go-i18n/v2 v2.2.2 github.com/nicksnyder/go-i18n/v2 v2.2.2
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/rakyll/statik v0.1.7 github.com/rakyll/statik v0.1.7
github.com/rs/cors v1.10.1 github.com/rs/cors v1.10.1
@@ -60,7 +60,7 @@ require (
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
github.com/ttacon/libphonenumber v1.2.1 github.com/ttacon/libphonenumber v1.2.1
github.com/zitadel/logging v0.5.0 github.com/zitadel/logging v0.5.0
github.com/zitadel/oidc/v3 v3.4.0 github.com/zitadel/oidc/v3 v3.5.0
github.com/zitadel/passwap v0.4.0 github.com/zitadel/passwap v0.4.0
github.com/zitadel/saml v0.1.2 github.com/zitadel/saml v0.1.2
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0
@@ -74,10 +74,10 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.20.0 go.opentelemetry.io/otel/sdk/metric v1.20.0
go.opentelemetry.io/otel/trace v1.21.0 go.opentelemetry.io/otel/trace v1.21.0
go.uber.org/mock v0.3.0 go.uber.org/mock v0.3.0
golang.org/x/crypto v0.15.0 golang.org/x/crypto v0.16.0
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678
golang.org/x/net v0.18.0 golang.org/x/net v0.19.0
golang.org/x/oauth2 v0.14.0 golang.org/x/oauth2 v0.15.0
golang.org/x/sync v0.5.0 golang.org/x/sync v0.5.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
google.golang.org/api v0.150.0 google.golang.org/api v0.150.0
@@ -107,6 +107,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
@@ -201,7 +202,7 @@ require (
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/sys v0.14.0 golang.org/x/sys v0.15.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

21
go.sum
View File

@@ -122,6 +122,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/brianvoe/gofakeit/v6 v6.25.0 h1:ZpFjktOpLZUeF8q223o0rUuXtA+m5qW5srjvVi+JkXk=
github.com/brianvoe/gofakeit/v6 v6.25.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@@ -865,8 +867,8 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA=
github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE=
github.com/zitadel/oidc/v3 v3.4.0 h1:JkbNnrk/7IG+NOBoZp/P0kx6tPcBvnCekSqDTPCOok4= github.com/zitadel/oidc/v3 v3.5.0 h1:z51AN6FPo5UuwYJ1r9nLvHlxpTGYd8QXg5MrtYm/dgM=
github.com/zitadel/oidc/v3 v3.4.0/go.mod h1:jUnLnx5ihKlo88cSEduZkKlzeMrjzcWVZ8fTzKBxZKY= github.com/zitadel/oidc/v3 v3.5.0/go.mod h1:R8sF5DPR98QQnOoyySsaNqI4NcF/VFMkf/XoYiBUuXQ=
github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0= github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0=
github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g= github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g=
github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo= github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo=
@@ -951,8 +953,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1040,8 +1042,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1051,8 +1053,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1133,8 +1135,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

File diff suppressed because it is too large Load Diff

View File

@@ -3,29 +3,23 @@ package admin
import ( import (
"context" "context"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/api/grpc/text" "github.com/zitadel/zitadel/internal/domain"
caos_errors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/i18n"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
) )
func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) { func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) {
langs, err := s.query.Languages(ctx) return &admin_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
if err != nil {
return nil, err
}
return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
} }
func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) { func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) {
lang, err := language.Parse(req.Language) lang, err := domain.ParseLanguage(req.Language)
if err != nil { if err != nil {
return nil, caos_errors.ThrowInvalidArgument(err, "API-39nnf", "Errors.Language.Parse") return nil, err
} }
details, err := s.command.SetDefaultLanguage(ctx, lang) details, err := s.command.SetDefaultLanguage(ctx, lang[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -0,0 +1,19 @@
package admin
import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func selectLanguagesToCommand(languages *admin.SelectLanguages) (tags []language.Tag, err error) {
allowedLanguages := languages.GetList()
if allowedLanguages == nil && languages != nil {
allowedLanguages = make([]string, 0)
}
if allowedLanguages == nil {
return nil, nil
}
return domain.ParseLanguage(allowedLanguages...)
}

View File

@@ -75,7 +75,6 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*
return nil, err return nil, err
} }
human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine
createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{ createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
Name: req.Org.Name, Name: req.Org.Name,
CustomDomain: req.Org.Domain, CustomDomain: req.Org.Domain,

View File

@@ -5,11 +5,19 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/admin"
) )
func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) { func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) {
details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration}) lang, err := selectLanguagesToCommand(req.GetAllowedLanguages())
if err != nil {
return nil, err
}
details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{
DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration,
AllowedLanguages: lang,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -26,5 +34,6 @@ func (s *Server) GetRestrictions(ctx context.Context, _ *admin.GetRestrictionsRe
return &admin.GetRestrictionsResponse{ return &admin.GetRestrictionsResponse{
Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner), Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner),
DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration, DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration,
AllowedLanguages: domain.LanguagesToStrings(restrictions.AllowedLanguages),
}, nil }, nil
} }

View File

@@ -6,6 +6,7 @@ import (
"bytes" "bytes"
"context" "context"
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"io" "io"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
@@ -29,19 +30,25 @@ func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
require.NoError(t, err) require.NoError(t, err)
browserSession := &http.Client{Jar: jar} browserSession := &http.Client{Jar: jar}
// Default should be allowed var csrfToken string
csrfToken := awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) t.Run("public org registration is allowed by default", func(*testing.T) {
csrfToken = awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
})
t.Run("disallowing public org registration disables the endpoints", func(*testing.T) {
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)}) _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
require.NoError(t, err) require.NoError(t, err)
awaitDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken) awaitPubOrgRegDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken)
})
t.Run("allowing public org registration again re-enables the endpoints", func(*testing.T) {
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)}) _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
require.NoError(t, err) require.NoError(t, err)
awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
})
} }
// awaitAllowed doesn't accept a CSRF token, as we expected it to always produce a new one // awaitPubOrgRegAllowed doesn't accept a CSRF token, as we expected it to always produce a new one
func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string { func awaitPubOrgRegAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string {
csrfToken := awaitGetResponse(t, ctx, client, parsedURL, http.StatusOK) csrfToken := awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusOK)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken) awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken)
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{}) restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err) require.NoError(t, err)
@@ -49,17 +56,17 @@ func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsed
return csrfToken return csrfToken
} }
// awaitDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore // awaitPubOrgRegDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
func awaitDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) { func awaitPubOrgRegDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
awaitGetResponse(t, ctx, client, parsedURL, http.StatusNotFound) awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken) awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken)
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{}) restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err) require.NoError(t, err)
require.True(t, restrictions.DisallowPublicOrgRegistration) require.True(t, restrictions.DisallowPublicOrgRegistration)
} }
// awaitGetResponse cuts the CSRF token from the response body if it exists // awaitGetSSRGetResponse cuts the CSRF token from the response body if it exists
func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string { func awaitGetSSRGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
var csrfToken []byte var csrfToken []byte
await(t, ctx, func() bool { await(t, ctx, func() bool {
resp, err := client.Get(parsedURL.String()) resp, err := client.Get(parsedURL.String())
@@ -71,7 +78,7 @@ func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, pa
if hasCsrfToken { if hasCsrfToken {
csrfToken, _, _ = bytes.Cut(after, []byte(`">`)) csrfToken, _, _ = bytes.Cut(after, []byte(`">`))
} }
return resp.StatusCode == expectCode return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode)
}) })
return string(csrfToken) return string(csrfToken)
} }
@@ -83,24 +90,6 @@ func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Clien
"gorilla.csrf.Token": {csrfToken}, "gorilla.csrf.Token": {csrfToken},
}) })
require.NoError(t, err) require.NoError(t, err)
return resp.StatusCode == expectCode return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode)
}) })
} }
func await(t *testing.T, ctx context.Context, cb func() bool) {
deadline, ok := ctx.Deadline()
require.True(t, ok, "context must have deadline")
require.Eventuallyf(
t,
func() bool {
defer func() {
require.Nil(t, recover(), "panic in await callback")
}()
return cb()
},
time.Until(deadline),
100*time.Millisecond,
"awaiting successful callback failed",
)
}

View File

@@ -0,0 +1,258 @@
//go:build integration
package admin_test
import (
"context"
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/text"
"github.com/zitadel/zitadel/pkg/grpc/user"
"golang.org/x/text/language"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"io"
"net/http"
"testing"
"time"
)
func TestServer_Restrictions_AllowedLanguages(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel()
var (
defaultAndAllowedLanguage = language.German
supportedLanguagesStr = []string{language.German.String(), language.English.String(), language.Japanese.String()}
disallowedLanguage = language.Spanish
unsupportedLanguage1 = language.Afrikaans
unsupportedLanguage2 = language.Albanian
)
domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(ctx, SystemCTX)
t.Run("assumed defaults are correct", func(tt *testing.T) {
tt.Run("languages are not restricted by default", func(ttt *testing.T) {
restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{})
require.NoError(ttt, err)
require.Len(ttt, restrictions.AllowedLanguages, 0)
})
tt.Run("default language is English by default", func(ttt *testing.T) {
defaultLang, err := Tester.Client.Admin.GetDefaultLanguage(iamOwnerCtx, &admin.GetDefaultLanguageRequest{})
require.NoError(ttt, err)
require.Equal(ttt, language.Make(defaultLang.Language), language.English)
})
tt.Run("the discovery endpoint returns all supported languages", func(ttt *testing.T) {
checkDiscoveryEndpoint(ttt, domain, supportedLanguagesStr, nil)
})
})
t.Run("restricting the default language fails", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: []string{defaultAndAllowedLanguage.String()}}})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
})
t.Run("not defining any restrictions throws an error", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.InvalidArgument, expectStatus.Code())
})
t.Run("setting the default language works", func(tt *testing.T) {
setAndAwaitDefaultLanguage(iamOwnerCtx, tt, defaultAndAllowedLanguage)
})
t.Run("restricting allowed languages works", func(tt *testing.T) {
setAndAwaitAllowedLanguages(iamOwnerCtx, tt, []string{defaultAndAllowedLanguage.String()})
})
t.Run("setting the default language to a disallowed language fails", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetDefaultLanguage(iamOwnerCtx, &admin.SetDefaultLanguageRequest{Language: disallowedLanguage.String()})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
})
t.Run("the list of supported languages includes the disallowed languages", func(tt *testing.T) {
supported, err := Tester.Client.Admin.GetSupportedLanguages(iamOwnerCtx, &admin.GetSupportedLanguagesRequest{})
require.NoError(tt, err)
require.Condition(tt, contains(supported.GetLanguages(), supportedLanguagesStr))
})
t.Run("the disallowed language is not listed in the discovery endpoint", func(tt *testing.T) {
checkDiscoveryEndpoint(tt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
})
t.Run("the login ui is rendered in the default language", func(tt *testing.T) {
checkLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Allgemeine Geschäftsbedingungen und Datenschutz")
})
t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) {
var importedUser *management.ImportHumanUserResponse
tt.Run("import user", func(ttt *testing.T) {
var err error
importedUser, err = importUser(iamOwnerCtx, unsupportedLanguage1)
require.NoError(ttt, err)
})
tt.Run("change user profile", func(ttt *testing.T) {
_, err := Tester.Client.Mgmt.UpdateHumanProfile(iamOwnerCtx, &management.UpdateHumanProfileRequest{
UserId: importedUser.GetUserId(),
FirstName: "hodor",
LastName: "hodor",
NickName: integration.RandString(5),
DisplayName: "hodor",
PreferredLanguage: unsupportedLanguage2.String(),
Gender: user.Gender_GENDER_MALE,
})
require.NoError(ttt, err)
})
})
t.Run("custom texts are only restricted by the supported languages", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetCustomLoginText(iamOwnerCtx, &admin.SetCustomLoginTextsRequest{
Language: disallowedLanguage.String(),
EmailVerificationText: &text.EmailVerificationScreenText{
Description: "hodor",
},
})
assert.NoError(tt, err)
_, err = Tester.Client.Mgmt.SetCustomLoginText(iamOwnerCtx, &management.SetCustomLoginTextsRequest{
Language: disallowedLanguage.String(),
EmailVerificationText: &text.EmailVerificationScreenText{
Description: "hodor",
},
})
assert.NoError(tt, err)
_, err = Tester.Client.Mgmt.SetCustomInitMessageText(iamOwnerCtx, &management.SetCustomInitMessageTextRequest{
Language: disallowedLanguage.String(),
Text: "hodor",
})
assert.NoError(tt, err)
_, err = Tester.Client.Admin.SetDefaultInitMessageText(iamOwnerCtx, &admin.SetDefaultInitMessageTextRequest{
Language: disallowedLanguage.String(),
Text: "hodor",
})
assert.NoError(tt, err)
})
t.Run("allowing all languages works", func(tt *testing.T) {
tt.Run("restricting allowed languages works", func(ttt *testing.T) {
setAndAwaitAllowedLanguages(iamOwnerCtx, ttt, make([]string, 0))
})
})
t.Run("allowing the language makes it usable again", func(tt *testing.T) {
tt.Run("the disallowed language is listed in the discovery endpoint again", func(ttt *testing.T) {
checkDiscoveryEndpoint(ttt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
})
tt.Run("the login ui is rendered in the allowed language", func(ttt *testing.T) {
checkLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Términos y condiciones")
})
})
}
func setAndAwaitAllowedLanguages(ctx context.Context, t *testing.T, selectLanguages []string) {
_, err := Tester.Client.Admin.SetRestrictions(ctx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: selectLanguages}})
require.NoError(t, err)
awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
defer awaitCancel()
await(t, awaitCtx, func() bool {
restrictions, getErr := Tester.Client.Admin.GetRestrictions(awaitCtx, &admin.GetRestrictionsRequest{})
expectLanguages := selectLanguages
if len(selectLanguages) == 0 {
expectLanguages = nil
}
return assert.NoError(NoopAssertionT, getErr) &&
assert.Equal(NoopAssertionT, expectLanguages, restrictions.GetAllowedLanguages())
})
}
func setAndAwaitDefaultLanguage(ctx context.Context, t *testing.T, lang language.Tag) {
_, err := Tester.Client.Admin.SetDefaultLanguage(ctx, &admin.SetDefaultLanguageRequest{Language: lang.String()})
require.NoError(t, err)
awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
defer awaitCancel()
await(t, awaitCtx, func() bool {
defaultLang, getErr := Tester.Client.Admin.GetDefaultLanguage(awaitCtx, &admin.GetDefaultLanguageRequest{})
return assert.NoError(NoopAssertionT, getErr) &&
assert.Equal(NoopAssertionT, lang.String(), defaultLang.GetLanguage())
})
}
func importUser(ctx context.Context, preferredLanguage language.Tag) (*management.ImportHumanUserResponse, error) {
random := integration.RandString(5)
return Tester.Client.Mgmt.ImportHumanUser(ctx, &management.ImportHumanUserRequest{
UserName: "integration-test-user_" + random,
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: "hodor",
LastName: "hodor",
NickName: "hodor",
PreferredLanguage: preferredLanguage.String(),
},
Email: &management.ImportHumanUserRequest_Email{
Email: random + "@hodor.hodor",
IsEmailVerified: true,
},
PasswordChangeRequired: false,
Password: "Password1!",
})
}
func checkDiscoveryEndpoint(t *testing.T, domain string, containsUILocales, notContainsUILocales []string) {
resp, err := http.Get("http://" + domain + ":8080/.well-known/openid-configuration")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
defer func() {
require.NoError(t, resp.Body.Close())
}()
require.NoError(t, err)
doc := struct {
UILocalesSupported []string `json:"ui_locales_supported"`
}{}
require.NoError(t, json.Unmarshal(body, &doc))
if containsUILocales != nil {
assert.Condition(NoopAssertionT, contains(doc.UILocalesSupported, containsUILocales))
}
if notContainsUILocales != nil {
assert.Condition(NoopAssertionT, not(contains(doc.UILocalesSupported, notContainsUILocales)))
}
}
func checkLoginUILanguage(t *testing.T, domain string, acceptLanguage language.Tag, expectLang language.Tag, containsText string) {
req, err := http.NewRequest(http.MethodGet, "http://"+domain+":8080/ui/login/register", nil)
req.Header.Set("Accept-Language", acceptLanguage.String())
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
defer func() {
require.NoError(t, resp.Body.Close())
}()
require.NoError(t, err)
assert.Containsf(t, string(body), containsText, "login ui language is in "+expectLang.String())
}
// We would love to use assert.Contains here, but it doesn't work with slices of strings
func contains(container []string, subset []string) assert.Comparison {
return func() bool {
if subset == nil {
return true
}
for _, str := range subset {
var found bool
for _, containerStr := range container {
if str == containerStr {
found = true
break
}
}
if !found {
return false
}
}
return true
}
}
func not(cmp assert.Comparison) assert.Comparison {
return func() bool {
return !cmp()
}
}

View File

@@ -8,12 +8,17 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
) )
var ( var (
AdminCTX, SystemCTX context.Context AdminCTX, SystemCTX context.Context
Tester *integration.Tester Tester *integration.Tester
// NoopAssertionT is useful in combination with assert.Eventuallyf to use testify assertions in a callback
NoopAssertionT = new(noopAssertionT)
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -30,3 +35,29 @@ func TestMain(m *testing.M) {
return m.Run() return m.Run()
}()) }())
} }
func await(t *testing.T, ctx context.Context, cb func() bool) {
deadline, ok := ctx.Deadline()
require.True(t, ok, "context must have deadline")
assert.Eventuallyf(
t,
func() bool {
defer func() {
// Panics are not recovered and don't mark the test as failed, so we need to do that ourselves
require.Nil(t, recover(), "panic in await callback")
}()
return cb()
},
time.Until(deadline),
100*time.Millisecond,
"awaiting successful callback failed",
)
}
var _ assert.TestingT = (*noopAssertionT)(nil)
type noopAssertionT struct{}
func (*noopAssertionT) FailNow() {}
func (*noopAssertionT) Errorf(string, ...interface{}) {}

View File

@@ -2,15 +2,12 @@ package auth
import ( import (
"context" "context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/api/grpc/text"
auth_pb "github.com/zitadel/zitadel/pkg/grpc/auth" auth_pb "github.com/zitadel/zitadel/pkg/grpc/auth"
) )
func (s *Server) GetSupportedLanguages(ctx context.Context, req *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) { func (s *Server) GetSupportedLanguages(context.Context, *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
langs, err := s.query.Languages(ctx) return &auth_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
if err != nil {
return nil, err
}
return &auth_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
} }

View File

@@ -2,15 +2,12 @@ package management
import ( import (
"context" "context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/api/grpc/text"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
) )
func (s *Server) GetSupportedLanguages(ctx context.Context, req *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) { func (s *Server) GetSupportedLanguages(context.Context, *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
langs, err := s.query.Languages(ctx) return &mgmt_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
if err != nil {
return nil, err
}
return &mgmt_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
} }

View File

@@ -220,8 +220,7 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) { func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
human := AddHumanUserRequestToAddHuman(req) human := AddHumanUserRequestToAddHuman(req)
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true) if err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true); err != nil {
if err != nil {
return nil, err return nil, err
} }
return &mgmt_pb.AddHumanUserResponse{ return &mgmt_pb.AddHumanUserResponse{

View File

@@ -55,14 +55,14 @@ func TestImport_and_Get(t *testing.T) {
// create unique names. // create unique names.
lastName := strconv.FormatInt(time.Now().Unix(), 10) lastName := strconv.FormatInt(time.Now().Unix(), 10)
userName := strings.Join([]string{firstName, lastName}, "_") userName := strings.Join([]string{firstName, lastName}, "_")
email := strings.Join([]string{userName, "zitadel.com"}, "@") email := strings.Join([]string{userName, "example.com"}, "@")
res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{ res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
UserName: userName, UserName: userName,
Profile: &management.ImportHumanUserRequest_Profile{ Profile: &management.ImportHumanUserRequest_Profile{
FirstName: firstName, FirstName: firstName,
LastName: lastName, LastName: lastName,
PreferredLanguage: language.Afrikaans.String(), PreferredLanguage: language.Japanese.String(),
Gender: user.Gender_GENDER_DIVERSE, Gender: user.Gender_GENDER_DIVERSE,
}, },
Email: &management.ImportHumanUserRequest_Email{ Email: &management.ImportHumanUserRequest_Email{
@@ -82,3 +82,21 @@ func TestImport_and_Get(t *testing.T) {
}) })
} }
} }
func TestImport_UnparsablePreferredLanguage(t *testing.T) {
random := integration.RandString(5)
_, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
UserName: random,
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: random,
LastName: random,
PreferredLanguage: "not valid",
Gender: user.Gender_GENDER_DIVERSE,
},
Email: &management.ImportHumanUserRequest_Email{
Email: random + "@example.com",
IsEmailVerified: true,
},
})
require.NoError(t, err)
}

View File

@@ -24,7 +24,7 @@ const (
) )
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor { func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor {
translator, err := newZitadelTranslator(language.English) translator, err := i18n.NewZitadelTranslator(language.English)
logging.OnError(err).Panic("unable to get translator") logging.OnError(err).Panic("unable to get translator")
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return setInstance(ctx, req, info, handler, verifier, headerName, translator, explicitInstanceIdServices...) return setInstance(ctx, req, info, handler, verifier, headerName, translator, explicitInstanceIdServices...)

View File

@@ -7,6 +7,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/i18n"
_ "github.com/zitadel/zitadel/internal/statik" _ "github.com/zitadel/zitadel/internal/statik"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
) )
@@ -18,17 +19,15 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc.
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
if loc, ok := resp.(localizers); ok && resp != nil { if loc, ok := resp.(localizers); ok && resp != nil {
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) translator, translatorError := getTranslator(ctx)
if translatorError != nil { if translatorError != nil {
logging.New().WithError(translatorError).Error("could not load translator")
return resp, err return resp, err
} }
translateFields(ctx, loc, translator) translateFields(ctx, loc, translator)
} }
if err != nil { if err != nil {
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) translator, translatorError := getTranslator(ctx)
if translatorError != nil { if translatorError != nil {
logging.New().WithError(translatorError).Error("could not load translator")
return resp, err return resp, err
} }
err = translateError(ctx, err, translator) err = translateError(ctx, err, translator)
@@ -36,3 +35,11 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc.
return resp, err return resp, err
} }
} }
func getTranslator(ctx context.Context) (*i18n.Translator, error) {
translator, err := i18n.NewZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
if err != nil {
logging.New().WithError(err).Error("could not load translator")
}
return translator, err
}

View File

@@ -4,10 +4,6 @@ import (
"context" "context"
"errors" "errors"
"github.com/rakyll/statik/fs"
"github.com/zitadel/logging"
"golang.org/x/text/language"
caos_errs "github.com/zitadel/zitadel/internal/errors" caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/i18n"
) )
@@ -39,14 +35,3 @@ func translateError(ctx context.Context, err error, translator *i18n.Translator)
} }
return err return err
} }
func newZitadelTranslator(defaultLanguage language.Tag) (*i18n.Translator, error) {
return translatorFromNamespace("zitadel", defaultLanguage)
}
func translatorFromNamespace(namespace string, defaultLanguage language.Tag) (*i18n.Translator, error) {
dir, err := fs.NewWithNamespace(namespace)
logging.WithFields("namespace", namespace).OnError(err).Panic("unable to get namespace")
return i18n.NewTranslator(dir, defaultLanguage, "")
}

View File

@@ -7,7 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/api/grpc/text" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
@@ -116,13 +117,9 @@ func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.G
} }
func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) {
langs, err := s.query.Languages(ctx)
if err != nil {
return nil, err
}
instance := authz.GetInstance(ctx) instance := authz.GetInstance(ctx)
return &settings.GetGeneralSettingsResponse{ return &settings.GetGeneralSettingsResponse{
SupportedLanguages: text.LanguageTagsToStrings(langs), SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()),
DefaultOrgId: instance.DefaultOrganisationID(), DefaultOrgId: instance.DefaultOrganisationID(),
DefaultLanguage: instance.DefaultLanguage().String(), DefaultLanguage: instance.DefaultLanguage().String(),
}, nil }, nil

View File

@@ -1,13 +0,0 @@
package text
import (
"golang.org/x/text/language"
)
func LanguageTagsToStrings(langs []language.Tag) []string {
result := make([]string, len(langs))
for i, lang := range langs {
result[i] = lang.String()
}
return result
}

View File

@@ -72,7 +72,7 @@ func MachineToPb(view *query.Machine) *user_pb.Machine {
return &user_pb.Machine{ return &user_pb.Machine{
Name: view.Name, Name: view.Name,
Description: view.Description, Description: view.Description,
HasSecret: view.HasSecret, HasSecret: view.Secret != nil,
AccessTokenType: AccessTokenTypeToPb(view.AccessTokenType), AccessTokenType: AccessTokenTypeToPb(view.AccessTokenType),
} }
} }

View File

@@ -24,6 +24,10 @@ func TestServer_RegisterPasskey(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
// We also need a user session
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
type args struct { type args struct {
ctx context.Context ctx context.Context
req *user.RegisterPasskeyRequest req *user.RegisterPasskeyRequest
@@ -95,14 +99,12 @@ func TestServer_RegisterPasskey(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
/* TODO: after we are able to obtain a Bearer token for a human user
https://github.com/zitadel/zitadel/issues/6022
{ {
name: "human user", name: "user setting its own passkey",
args: args{ args: args{
ctx: CTX, ctx: Tester.WithAuthorizationToken(CTX, sessionToken),
req: &user.RegisterPasskeyRequest{ req: &user.RegisterPasskeyRequest{
UserId: humanUserID, UserId: userID,
}, },
}, },
want: &user.RegisterPasskeyResponse{ want: &user.RegisterPasskeyResponse{
@@ -111,7 +113,6 @@ func TestServer_RegisterPasskey(t *testing.T) {
}, },
}, },
}, },
*/
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@@ -5,16 +5,22 @@ package user_test
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
) )
func TestServer_RegisterTOTP(t *testing.T) { func TestServer_RegisterTOTP(t *testing.T) {
// userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
type args struct { type args struct {
ctx context.Context ctx context.Context
@@ -29,7 +35,7 @@ func TestServer_RegisterTOTP(t *testing.T) {
{ {
name: "missing user id", name: "missing user id",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.RegisterTOTPRequest{}, req: &user.RegisterTOTPRequest{},
}, },
wantErr: true, wantErr: true,
@@ -37,19 +43,17 @@ func TestServer_RegisterTOTP(t *testing.T) {
{ {
name: "user mismatch", name: "user mismatch",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.RegisterTOTPRequest{ req: &user.RegisterTOTPRequest{
UserId: "wrong", UserId: "wrong",
}, },
}, },
wantErr: true, wantErr: true,
}, },
/* TODO: after we are able to obtain a Bearer token for a human user
https://github.com/zitadel/zitadel/issues/6022
{ {
name: "human user", name: "success",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.RegisterTOTPRequest{ req: &user.RegisterTOTPRequest{
UserId: userID, UserId: userID,
}, },
@@ -60,7 +64,6 @@ func TestServer_RegisterTOTP(t *testing.T) {
}, },
}, },
}, },
*/
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -80,15 +83,16 @@ func TestServer_RegisterTOTP(t *testing.T) {
func TestServer_VerifyTOTPRegistration(t *testing.T) { func TestServer_VerifyTOTPRegistration(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
/* TODO: after we are able to obtain a Bearer token for a human user reg, err := Client.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
reg, err := Client.RegisterTOTP(CTX, &user.RegisterTOTPRequest{
UserId: userID, UserId: userID,
}) })
require.NoError(t, err) require.NoError(t, err)
code, err := totp.GenerateCode(reg.Secret, time.Now()) code, err := totp.GenerateCode(reg.Secret, time.Now())
require.NoError(t, err) require.NoError(t, err)
*/
type args struct { type args struct {
ctx context.Context ctx context.Context
@@ -103,7 +107,7 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
{ {
name: "user mismatch", name: "user mismatch",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.VerifyTOTPRegistrationRequest{ req: &user.VerifyTOTPRegistrationRequest{
UserId: "wrong", UserId: "wrong",
}, },
@@ -113,7 +117,7 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
{ {
name: "wrong code", name: "wrong code",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.VerifyTOTPRegistrationRequest{ req: &user.VerifyTOTPRegistrationRequest{
UserId: userID, UserId: userID,
Code: "123", Code: "123",
@@ -121,12 +125,10 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
/* TODO: after we are able to obtain a Bearer token for a human user
https://github.com/zitadel/zitadel/issues/6022
{ {
name: "success", name: "success",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.VerifyTOTPRegistrationRequest{ req: &user.VerifyTOTPRegistrationRequest{
UserId: userID, UserId: userID,
Code: code, Code: code,
@@ -138,7 +140,6 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
}, },
}, },
}, },
*/
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@@ -11,12 +11,17 @@ import (
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
) )
func TestServer_RegisterU2F(t *testing.T) { func TestServer_RegisterU2F(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
// We also need a user session
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
type args struct { type args struct {
ctx context.Context ctx context.Context
req *user.RegisterU2FRequest req *user.RegisterU2FRequest
@@ -45,12 +50,10 @@ func TestServer_RegisterU2F(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
/* TODO: after we are able to obtain a Bearer token for a human user
https://github.com/zitadel/zitadel/issues/6022
{ {
name: "human user", name: "user setting its own passkey",
args: args{ args: args{
ctx: CTX, ctx: Tester.WithAuthorizationToken(CTX, sessionToken),
req: &user.RegisterU2FRequest{ req: &user.RegisterU2FRequest{
UserId: userID, UserId: userID,
}, },
@@ -61,7 +64,6 @@ func TestServer_RegisterU2F(t *testing.T) {
}, },
}, },
}, },
*/
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -85,8 +87,11 @@ func TestServer_RegisterU2F(t *testing.T) {
func TestServer_VerifyU2FRegistration(t *testing.T) { func TestServer_VerifyU2FRegistration(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
/* TODO after we are able to obtain a Bearer token for a human user Tester.RegisterUserPasskey(CTX, userID)
pkr, err := Client.RegisterU2F(CTX, &user.RegisterU2FRequest{ _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{
UserId: userID, UserId: userID,
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -94,7 +99,6 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err) require.NoError(t, err)
*/
type args struct { type args struct {
ctx context.Context ctx context.Context
@@ -109,7 +113,7 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
{ {
name: "missing user id", name: "missing user id",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.VerifyU2FRegistrationRequest{ req: &user.VerifyU2FRegistrationRequest{
U2FId: "123", U2FId: "123",
TokenName: "nice name", TokenName: "nice name",
@@ -117,11 +121,10 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
/* TODO after we are able to obtain a Bearer token for a human user
{ {
name: "success", name: "success",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.VerifyU2FRegistrationRequest{ req: &user.VerifyU2FRegistrationRequest{
UserId: userID, UserId: userID,
U2FId: pkr.GetU2FId(), U2FId: pkr.GetU2FId(),
@@ -135,11 +138,10 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
}, },
}, },
}, },
*/
{ {
name: "wrong credential", name: "wrong credential",
args: args{ args: args{
ctx: CTX, ctx: ctx,
req: &user.VerifyU2FRegistrationRequest{ req: &user.VerifyU2FRegistrationRequest{
UserId: userID, UserId: userID,
U2FId: "123", U2FId: "123",

View File

@@ -28,8 +28,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
return nil, err return nil, err
} }
orgID := authz.GetCtxData(ctx).OrgID orgID := authz.GetCtxData(ctx).OrgID
err = s.command.AddHuman(ctx, orgID, human, false) if err = s.command.AddHuman(ctx, orgID, human, false); err != nil {
if err != nil {
return nil, err return nil, err
} }
return &user.AddHumanUserResponse{ return &user.AddHumanUserResponse{

View File

@@ -677,7 +677,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
parametersEqual: map[string]string{ parametersEqual: map[string]string{
"client_id": "clientID", "client_id": "clientID",
"prompt": "select_account", "prompt": "select_account",
"redirect_uri": "http://localhost:8080/idps/callback", "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback",
"response_type": "code", "response_type": "code",
"scope": "openid profile email", "scope": "openid profile email",
}, },
@@ -704,7 +704,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(), ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID, ResourceOwner: Tester.Organisation.ID,
}, },
url: "http://localhost:8000/sso", url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"}, parametersExisting: []string{"RelayState", "SAMLRequest"},
}, },
wantErr: false, wantErr: false,
@@ -728,7 +728,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(), ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID, ResourceOwner: Tester.Organisation.ID,
}, },
url: "http://localhost:8000/sso", url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"}, parametersExisting: []string{"RelayState", "SAMLRequest"},
}, },
wantErr: false, wantErr: false,

View File

@@ -8,7 +8,6 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/rakyll/statik/fs"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"golang.org/x/text/language" "golang.org/x/text/language"
@@ -112,7 +111,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) {
if err != nil { if err != nil {
return "", err return "", err
} }
host = u.Hostname() host = u.Host
if host == "" { if host == "" {
err = errors.New("empty host") err = errors.New("empty host")
} }
@@ -120,10 +119,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) {
} }
func newZitadelTranslator() *i18n.Translator { func newZitadelTranslator() *i18n.Translator {
dir, err := fs.NewWithNamespace("zitadel") translator, err := i18n.NewZitadelTranslator(language.English)
logging.WithFields("namespace", "zitadel").OnError(err).Panic("unable to get namespace")
translator, err := i18n.NewTranslator(dir, language.English, "")
logging.OnError(err).Panic("unable to get translator") logging.OnError(err).Panic("unable to get translator")
return translator return translator
} }

View File

@@ -221,7 +221,7 @@ func Test_setInstance(t *testing.T) {
r.Header.Set("host", "fromrequest") r.Header.Set("host", "fromrequest")
return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999")) return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999"))
}(), }(),
verifier: &mockInstanceVerifier{"fromorigin"}, verifier: &mockInstanceVerifier{"fromorigin:9999"},
headerName: "host", headerName: "host",
}, },
res{ res{

View File

@@ -0,0 +1,18 @@
package middleware
import (
"testing"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/i18n"
)
var (
SupportedLanguages = []language.Tag{language.English, language.German}
)
func TestMain(m *testing.M) {
i18n.SupportLanguages(SupportedLanguages...)
m.Run()
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
errz "github.com/zitadel/zitadel/internal/errors" zerrors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/user/model" "github.com/zitadel/zitadel/internal/user/model"
) )
@@ -55,7 +55,7 @@ func (s *Server) verifyAccessToken(ctx context.Context, tkn string) (*accessToke
token, err := s.repo.TokenByIDs(ctx, subject, tokenID) token, err := s.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil { if err != nil {
return nil, errz.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired") return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired")
} }
return accessTokenV1(tokenID, subject, token), nil return accessTokenV1(tokenID, subject, token), nil
} }
@@ -91,7 +91,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke
token.audience = append(token.audience, clientID) token.audience = append(token.audience, clientID)
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID) projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
if err != nil { if err != nil {
return errz.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
} }
roles, err := s.query.SearchProjectRoles(ctx, s.features.TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) roles, err := s.query.SearchProjectRoles(ctx, s.features.TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
if err != nil { if err != nil {

View File

@@ -17,6 +17,7 @@ import (
http_utils "github.com/zitadel/zitadel/internal/api/http" http_utils "github.com/zitadel/zitadel/internal/api/http"
oidc_api "github.com/zitadel/zitadel/internal/api/oidc" oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/integration"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
) )
@@ -500,8 +501,7 @@ func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDT
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
codeVerifier := "codeVerifier" return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(integration.CodeVerifier))
return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(codeVerifier))
} }
func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) { func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) {

View File

@@ -43,32 +43,14 @@ const (
func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Client, err error) { func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Client, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
client, err := o.query.AppByOIDCClientID(ctx, id) client, err := o.query.GetOIDCClientByID(ctx, id, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if client.State != domain.AppStateActive { if client.State != domain.AppStateActive {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active") return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active")
} }
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(client.ProjectID) return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2), nil
if err != nil {
return nil, errors.ThrowInternal(err, "OIDC-mPxqP", "Errors.Internal")
}
projectRoles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
if err != nil {
return nil, err
}
allowedScopes := make([]string, len(projectRoles.ProjectRoles))
for i, role := range projectRoles.ProjectRoles {
allowedScopes[i] = ScopeProjectRolePrefix + role.Key
}
accessTokenLifetime, idTokenLifetime, _, _, err := o.getOIDCSettings(ctx)
if err != nil {
return nil, err
}
return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2, accessTokenLifetime, idTokenLifetime, allowedScopes)
} }
func (o *OPStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) { func (o *OPStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) {
@@ -235,22 +217,10 @@ func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID
}, nil }, nil
} }
func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) { // ClientCredentials method is kept to keep the storage interface implemented.
loginname, err := query.NewUserLoginNamesSearchQuery(clientID) // However, it should never be called as the VerifyClient method on the Server is overridden.
if err != nil { func (o *OPStorage) ClientCredentials(context.Context, string, string) (op.Client, error) {
return nil, err return nil, errors.ThrowInternal(nil, "OIDC-Su8So", "Errors.Internal")
}
user, err := o.query.GetUser(ctx, false, loginname)
if err != nil {
return nil, err
}
if _, err := o.command.VerifyMachineSecret(ctx, user.ID, user.ResourceOwner, clientSecret); err != nil {
return nil, err
}
return &clientCredentialsClient{
id: clientID,
tokenType: accessTokenTypeToOIDC(user.Machine.AccessTokenType),
}, nil
} }
// isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin // isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin
@@ -934,3 +904,67 @@ func userinfoClaims(userInfo *oidc.UserInfo) func(c *actions.FieldConfig) interf
return c.Runtime.ToValue(claims) return c.Runtime.ToValue(claims)
} }
} }
func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if oidc.GrantType(r.Form.Get("grant_type")) == oidc.GrantTypeClientCredentials {
return s.clientCredentialsAuth(ctx, r.Data.ClientID, r.Data.ClientSecret)
}
clientID, assertion, err := clientIDFromCredentials(r.Data)
if err != nil {
return nil, err
}
client, err := s.query.GetOIDCClientByID(ctx, clientID, assertion)
if errors.IsNotFound(err) {
return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found")
}
if err != nil {
return nil, err // defaults to server error
}
if client.State != domain.AppStateActive {
return nil, oidc.ErrInvalidClient().WithDescription("client is not active")
}
switch client.AuthMethodType {
case domain.OIDCAuthMethodTypeBasic, domain.OIDCAuthMethodTypePost:
err = s.verifyClientSecret(ctx, client, r.Data.ClientSecret)
case domain.OIDCAuthMethodTypePrivateKeyJWT:
err = s.verifyClientAssertion(ctx, client, r.Data.ClientAssertion)
case domain.OIDCAuthMethodTypeNone:
}
if err != nil {
return nil, err
}
return ClientFromBusiness(client, s.defaultLoginURL, s.defaultLoginURLV2), nil
}
func (s *Server) verifyClientAssertion(ctx context.Context, client *query.OIDCClient, assertion string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if assertion == "" {
return oidc.ErrInvalidClient().WithDescription("empty client assertion")
}
verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, client.ClockSkew)
if _, err := op.VerifyJWTAssertion(ctx, assertion, verifier); err != nil {
return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid assertion")
}
return nil
}
func (s *Server) verifyClientSecret(ctx context.Context, client *query.OIDCClient, secret string) (err error) {
_, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if secret == "" {
return oidc.ErrInvalidClient().WithDescription("empty client secret")
}
if err = crypto.CompareHash(client.ClientSecret, []byte(secret), s.hashAlg); err != nil {
return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid secret")
}
return nil
}

View File

@@ -1,6 +1,7 @@
package oidc package oidc
import ( import (
"slices"
"strings" "strings"
"time" "time"
@@ -9,43 +10,40 @@ import (
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
) )
type Client struct { type Client struct {
app *query.App client *query.OIDCClient
defaultLoginURL string defaultLoginURL string
defaultLoginURLV2 string defaultLoginURLV2 string
defaultAccessTokenLifetime time.Duration
defaultIdTokenLifetime time.Duration
allowedScopes []string allowedScopes []string
} }
func ClientFromBusiness(app *query.App, defaultLoginURL, defaultLoginURLV2 string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) { func ClientFromBusiness(client *query.OIDCClient, defaultLoginURL, defaultLoginURLV2 string) op.Client {
if app.OIDCConfig == nil { allowedScopes := make([]string, len(client.ProjectRoleKeys))
return nil, errors.ThrowInvalidArgument(nil, "OIDC-d5bhD", "client is not a proper oidc application") for i, roleKey := range client.ProjectRoleKeys {
allowedScopes[i] = ScopeProjectRolePrefix + roleKey
} }
return &Client{ return &Client{
app: app, client: client,
defaultLoginURL: defaultLoginURL, defaultLoginURL: defaultLoginURL,
defaultLoginURLV2: defaultLoginURLV2, defaultLoginURLV2: defaultLoginURLV2,
defaultAccessTokenLifetime: defaultAccessTokenLifetime, allowedScopes: allowedScopes,
defaultIdTokenLifetime: defaultIdTokenLifetime, }
allowedScopes: allowedScopes},
nil
} }
func (c *Client) ApplicationType() op.ApplicationType { func (c *Client) ApplicationType() op.ApplicationType {
return op.ApplicationType(c.app.OIDCConfig.AppType) return op.ApplicationType(c.client.ApplicationType)
} }
func (c *Client) AuthMethod() oidc.AuthMethod { func (c *Client) AuthMethod() oidc.AuthMethod {
return authMethodToOIDC(c.app.OIDCConfig.AuthMethodType) return authMethodToOIDC(c.client.AuthMethodType)
} }
func (c *Client) GetID() string { func (c *Client) GetID() string {
return c.app.OIDCConfig.ClientID return c.client.ClientID
} }
func (c *Client) LoginURL(id string) string { func (c *Client) LoginURL(id string) string {
@@ -56,28 +54,28 @@ func (c *Client) LoginURL(id string) string {
} }
func (c *Client) RedirectURIs() []string { func (c *Client) RedirectURIs() []string {
return c.app.OIDCConfig.RedirectURIs return c.client.RedirectURIs
} }
func (c *Client) PostLogoutRedirectURIs() []string { func (c *Client) PostLogoutRedirectURIs() []string {
return c.app.OIDCConfig.PostLogoutRedirectURIs return c.client.PostLogoutRedirectURIs
} }
func (c *Client) ResponseTypes() []oidc.ResponseType { func (c *Client) ResponseTypes() []oidc.ResponseType {
return responseTypesToOIDC(c.app.OIDCConfig.ResponseTypes) return responseTypesToOIDC(c.client.ResponseTypes)
} }
func (c *Client) GrantTypes() []oidc.GrantType { func (c *Client) GrantTypes() []oidc.GrantType {
return grantTypesToOIDC(c.app.OIDCConfig.GrantTypes) return grantTypesToOIDC(c.client.GrantTypes)
} }
func (c *Client) DevMode() bool { func (c *Client) DevMode() bool {
return c.app.OIDCConfig.IsDevMode return c.client.IsDevMode
} }
func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string { return func(scopes []string) []string {
if c.app.OIDCConfig.AssertIDTokenRole { if c.client.IDTokenRoleAssertion {
return scopes return scopes
} }
return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix)
@@ -86,7 +84,7 @@ func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []strin
func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string { return func(scopes []string) []string {
if c.app.OIDCConfig.AssertAccessTokenRole { if c.client.AccessTokenRoleAssertion {
return scopes return scopes
} }
return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix)
@@ -94,15 +92,15 @@ func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []s
} }
func (c *Client) AccessTokenLifetime() time.Duration { func (c *Client) AccessTokenLifetime() time.Duration {
return c.defaultAccessTokenLifetime //PLANNED: impl from real client return c.client.AccessTokenLifetime
} }
func (c *Client) IDTokenLifetime() time.Duration { func (c *Client) IDTokenLifetime() time.Duration {
return c.defaultIdTokenLifetime //PLANNED: impl from real client return c.client.IDTokenLifetime
} }
func (c *Client) AccessTokenType() op.AccessTokenType { func (c *Client) AccessTokenType() op.AccessTokenType {
return accessTokenTypeToOIDC(c.app.OIDCConfig.AccessTokenType) return accessTokenTypeToOIDC(c.client.AccessTokenType)
} }
func (c *Client) IsScopeAllowed(scope string) bool { func (c *Client) IsScopeAllowed(scope string) bool {
@@ -127,20 +125,15 @@ func (c *Client) IsScopeAllowed(scope string) bool {
if scope == ScopeProjectsRoles { if scope == ScopeProjectsRoles {
return true return true
} }
for _, allowedScope := range c.allowedScopes { return slices.Contains(c.allowedScopes, scope)
if scope == allowedScope {
return true
}
}
return false
} }
func (c *Client) ClockSkew() time.Duration { func (c *Client) ClockSkew() time.Duration {
return c.app.OIDCConfig.ClockSkew return c.client.ClockSkew
} }
func (c *Client) IDTokenUserinfoClaimsAssertion() bool { func (c *Client) IDTokenUserinfoClaimsAssertion() bool {
return c.app.OIDCConfig.AssertIDTokenUserinfo return c.client.IDTokenUserinfoAssertion
} }
func accessTokenTypeToOIDC(tokenType domain.OIDCTokenType) op.AccessTokenType { func accessTokenTypeToOIDC(tokenType domain.OIDCTokenType) op.AccessTokenType {
@@ -229,3 +222,14 @@ func removeScopeWithPrefix(scopes []string, scopePrefix ...string) []string {
} }
return newScopeList return newScopeList
} }
func clientIDFromCredentials(cc *op.ClientCredentials) (clientID string, assertion bool, err error) {
if cc.ClientAssertion != "" {
claims := new(oidc.JWTTokenRequest)
if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil {
return "", false, oidc.ErrInvalidClient().WithParent(err)
}
return claims.Issuer, true, nil
}
return cc.ClientID, false, nil
}

View File

@@ -1,10 +1,15 @@
package oidc package oidc
import ( import (
"context"
"time" "time"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
) )
type clientCredentialsRequest struct { type clientCredentialsRequest struct {
@@ -28,15 +33,42 @@ func (c *clientCredentialsRequest) GetScopes() []string {
return c.scopes return c.scopes
} }
func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
searchQuery, err := query.NewUserLoginNamesSearchQuery(clientID)
if err != nil {
return nil, err
}
user, err := s.query.GetUser(ctx, false, searchQuery)
if errors.IsNotFound(err) {
return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found")
}
if err != nil {
return nil, err // defaults to server error
}
if user.Machine == nil || user.Machine.Secret == nil {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-pieP8", "Errors.User.Machine.Secret.NotExisting")
}
if err = crypto.CompareHash(user.Machine.Secret, []byte(clientSecret), s.hashAlg); err != nil {
s.command.MachineSecretCheckFailed(ctx, user.ID, user.ResourceOwner)
return nil, errors.ThrowInvalidArgument(err, "OIDC-VoXo6", "Errors.User.Machine.Secret.Invalid")
}
s.command.MachineSecretCheckSucceeded(ctx, user.ID, user.ResourceOwner)
return &clientCredentialsClient{
id: clientID,
user: user,
}, nil
}
type clientCredentialsClient struct { type clientCredentialsClient struct {
id string id string
tokenType op.AccessTokenType user *query.User
} }
// AccessTokenType returns the AccessTokenType for the token to be created because of the client credentials request // AccessTokenType returns the AccessTokenType for the token to be created because of the client credentials request
// machine users currently only have opaque tokens ([op.AccessTokenTypeBearer]) // machine users currently only have opaque tokens ([op.AccessTokenTypeBearer])
func (c *clientCredentialsClient) AccessTokenType() op.AccessTokenType { func (c *clientCredentialsClient) AccessTokenType() op.AccessTokenType {
return c.tokenType return accessTokenTypeToOIDC(c.user.Machine.AccessTokenType)
} }
// GetID returns the client_id (username of the machine user) for the token to be created because of the client credentials request // GetID returns the client_id (username of the machine user) for the token to be created because of the client credentials request

View File

@@ -4,20 +4,26 @@ package oidc_test
import ( import (
"context" "context"
"fmt"
"testing" "testing"
"time" "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/client/rs" "github.com/zitadel/oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/text/language" "golang.org/x/text/language"
oidc_api "github.com/zitadel/zitadel/internal/api/oidc" oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/authn"
"github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/management"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user"
) )
func TestOPStorage_SetUserinfoFromToken(t *testing.T) { func TestOPStorage_SetUserinfoFromToken(t *testing.T) {
@@ -142,3 +148,245 @@ func assertIntrospection(
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"name"]) assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"name"])
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"primary_domain"]) assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"primary_domain"])
} }
// TestServer_VerifyClient tests verification by running code flow tests
// with clients that have different authentication methods.
func TestServer_VerifyClient(t *testing.T) {
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
inactiveClient, err := Tester.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
require.NoError(t, err)
nativeClient, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
require.NoError(t, err)
basicWebClient, err := Tester.CreateOIDCWebClientBasic(CTX, redirectURI, logoutRedirectURI, project.GetId())
require.NoError(t, err)
jwtWebClient, keyData, err := Tester.CreateOIDCWebClientJWT(CTX, redirectURI, logoutRedirectURI, project.GetId())
require.NoError(t, err)
type clientDetails struct {
authReqClientID string
clientID string
clientSecret string
keyData []byte
}
tests := []struct {
name string
client clientDetails
wantErr bool
}{
{
name: "empty client ID error",
client: clientDetails{
authReqClientID: nativeClient.GetClientId(),
},
wantErr: true,
},
{
name: "client not found error",
client: clientDetails{
authReqClientID: nativeClient.GetClientId(),
clientID: "foo",
},
wantErr: true,
},
{
name: "client inactive error",
client: clientDetails{
authReqClientID: nativeClient.GetClientId(),
clientID: inactiveClient.GetClientId(),
},
wantErr: true,
},
{
name: "native client success",
client: clientDetails{
authReqClientID: nativeClient.GetClientId(),
clientID: nativeClient.GetClientId(),
},
},
{
name: "web client basic secret empty error",
client: clientDetails{
authReqClientID: basicWebClient.GetClientId(),
clientID: basicWebClient.GetClientId(),
clientSecret: "",
},
wantErr: true,
},
{
name: "web client basic secret invalid error",
client: clientDetails{
authReqClientID: basicWebClient.GetClientId(),
clientID: basicWebClient.GetClientId(),
clientSecret: "wrong",
},
wantErr: true,
},
{
name: "web client basic secret success",
client: clientDetails{
authReqClientID: basicWebClient.GetClientId(),
clientID: basicWebClient.GetClientId(),
clientSecret: basicWebClient.GetClientSecret(),
},
},
{
name: "web client JWT profile empty assertion error",
client: clientDetails{
authReqClientID: jwtWebClient.GetClientId(),
clientID: jwtWebClient.GetClientId(),
},
wantErr: true,
},
{
name: "web client JWT profile invalid assertion error",
client: clientDetails{
authReqClientID: jwtWebClient.GetClientId(),
clientID: jwtWebClient.GetClientId(),
keyData: createInvalidKeyData(t, jwtWebClient),
},
wantErr: true,
},
{
name: "web client JWT profile success",
client: clientDetails{
authReqClientID: jwtWebClient.GetClientId(),
clientID: jwtWebClient.GetClientId(),
keyData: keyData,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fmt.Printf("\n\n%s\n\n", tt.client.keyData)
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, tt.client.authReqClientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, oidc.ScopeOpenID)
require.NoError(t, err)
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
// use a new RP so we can inject different credentials
var options []rp.Option
if tt.client.keyData != nil {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyFile(tt.client.keyData)))
}
provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.client.clientID, tt.client.clientSecret, redirectURI, []string{oidc.ScopeOpenID}, options...)
require.NoError(t, err)
// test code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
codeOpts := codeExchangeOptions(t, provider)
tokens, err := rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, codeOpts...)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertTokens(t, tokens, false)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
})
}
}
func codeExchangeOptions(t testing.TB, provider rp.RelyingParty) []rp.CodeExchangeOpt {
codeOpts := []rp.CodeExchangeOpt{rp.WithCodeVerifier(integration.CodeVerifier)}
if signer := provider.Signer(); signer != nil {
assertion, err := client.SignedJWTProfileAssertion(provider.OAuthConfig().ClientID, []string{provider.Issuer()}, time.Hour, provider.Signer())
require.NoError(t, err)
codeOpts = append(codeOpts, rp.WithClientAssertionJWT(assertion))
}
return codeOpts
}
func createInvalidKeyData(t testing.TB, client *management.AddOIDCAppResponse) []byte {
key := domain.ApplicationKey{
Type: domain.AuthNKeyTypeJSON,
KeyID: "1",
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"),
ApplicationID: client.GetAppId(),
ClientID: client.GetClientId(),
}
data, err := key.Detail()
require.NoError(t, err)
return data
}
func TestServer_CreateAccessToken_ClientCredentials(t *testing.T) {
clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX)
require.NoError(t, err)
type clientDetails struct {
clientID string
clientSecret string
keyData []byte
}
tests := []struct {
name string
clientID string
clientSecret string
wantErr bool
}{
{
name: "missing client ID error",
clientID: "",
clientSecret: clientSecret,
wantErr: true,
},
{
name: "client not found error",
clientID: "foo",
clientSecret: clientSecret,
wantErr: true,
},
{
name: "machine user without secret error",
clientID: func() string {
name := gofakeit.Username()
_, err := Tester.Client.Mgmt.AddMachineUser(CTX, &management.AddMachineUserRequest{
Name: name,
UserName: name,
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
})
require.NoError(t, err)
return name
}(),
clientSecret: clientSecret,
wantErr: true,
},
{
name: "wrong secret error",
clientID: clientID,
clientSecret: "bar",
wantErr: true,
},
{
name: "success",
clientID: clientID,
clientSecret: clientSecret,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.clientID, tt.clientSecret, redirectURI, []string{oidc.ScopeOpenID})
require.NoError(t, err)
tokens, err := rp.ClientCredentials(CTX, provider, nil)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotNil(t, tokens)
assert.NotEmpty(t, tokens.AccessToken)
})
}
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
errz "github.com/zitadel/zitadel/internal/errors" zerrors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
) )
@@ -31,14 +31,14 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
clientChan := make(chan *instrospectionClientResult) clientChan := make(chan *introspectionClientResult)
go s.instrospectionClientAuth(ctx, r.Data.ClientCredentials, clientChan) go s.introspectionClientAuth(ctx, r.Data.ClientCredentials, clientChan)
tokenChan := make(chan *introspectionTokenResult) tokenChan := make(chan *introspectionTokenResult)
go s.introspectionToken(ctx, r.Data.Token, tokenChan) go s.introspectionToken(ctx, r.Data.Token, tokenChan)
var ( var (
client *instrospectionClientResult client *introspectionClientResult
token *introspectionTokenResult token *introspectionTokenResult
) )
@@ -116,13 +116,13 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
return op.NewResponse(introspectionResp), nil return op.NewResponse(introspectionResp), nil
} }
type instrospectionClientResult struct { type introspectionClientResult struct {
clientID string clientID string
projectID string projectID string
err error err error
} }
func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *instrospectionClientResult) { func (s *Server) introspectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *introspectionClientResult) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
clientID, projectID, err := func() (string, string, error) { clientID, projectID, err := func() (string, string, error) {
@@ -147,7 +147,7 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred
span.EndWithError(err) span.EndWithError(err)
rc <- &instrospectionClientResult{ rc <- &introspectionClientResult{
clientID: clientID, clientID: clientID,
projectID: projectID, projectID: projectID,
err: err, err: err,
@@ -157,15 +157,11 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred
// clientFromCredentials parses the client ID early, // clientFromCredentials parses the client ID early,
// and makes a single query for the client for either auth methods. // and makes a single query for the client for either auth methods.
func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredentials) (client *query.IntrospectionClient, err error) { func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredentials) (client *query.IntrospectionClient, err error) {
if cc.ClientAssertion != "" { clientID, assertion, err := clientIDFromCredentials(cc)
claims := new(oidc.JWTTokenRequest) if err != nil {
if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil { return nil, err
return nil, oidc.ErrUnauthorizedClient().WithParent(err)
}
client, err = s.query.GetIntrospectionClientByID(ctx, claims.Issuer, true)
} else {
client, err = s.query.GetIntrospectionClientByID(ctx, cc.ClientID, false)
} }
client, err = s.query.GetIntrospectionClientByID(ctx, clientID, assertion)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, oidc.ErrUnauthorizedClient().WithParent(err) return nil, oidc.ErrUnauthorizedClient().WithParent(err)
} }
@@ -196,5 +192,5 @@ func validateIntrospectionAudience(audience []string, clientID, projectID string
return nil return nil
} }
return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client") return zerrors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
} }

View File

@@ -31,9 +31,9 @@ var (
) )
const ( const (
redirectURI = "oidcintegrationtest://callback" redirectURI = "https://callback"
redirectURIImplicit = "http://localhost:9999/callback" redirectURIImplicit = "http://localhost:9999/callback"
logoutRedirectURI = "oidcintegrationtest://logged-out" logoutRedirectURI = "https://logged-out"
zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix
) )

View File

@@ -6,11 +6,9 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/rakyll/statik/fs"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op"
"golang.org/x/exp/slog" "golang.org/x/exp/slog"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/assets" "github.com/zitadel/zitadel/internal/api/assets"
http_utils "github.com/zitadel/zitadel/internal/api/http" http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -23,7 +21,6 @@ import (
caos_errs "github.com/zitadel/zitadel/internal/errors" caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/telemetry/metrics"
) )
@@ -131,6 +128,9 @@ func NewServer(
query: query, query: query,
command: command, command: command,
keySet: newKeySet(context.TODO(), time.Hour, query.GetActivePublicKeyByID), keySet: newKeySet(context.TODO(), time.Hour, query.GetActivePublicKeyByID),
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
defaultLoginURLV2: config.DefaultLoginURLV2,
defaultLogoutURLV2: config.DefaultLogoutURLV2,
fallbackLogger: fallbackLogger, fallbackLogger: fallbackLogger,
hashAlg: crypto.NewBCrypt(10), // as we are only verifying in oidc, the cost is already part of the hash string and the config here is irrelevant. hashAlg: crypto.NewBCrypt(10), // as we are only verifying in oidc, the cost is already part of the hash string and the config here is irrelevant.
signingKeyAlgorithm: config.SigningKeyAlgorithm, signingKeyAlgorithm: config.SigningKeyAlgorithm,
@@ -167,10 +167,6 @@ func ignoredQuotaLimitEndpoint(endpoints *EndpointConfig) []string {
} }
func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) { func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) {
supportedLanguages, err := getSupportedLanguages()
if err != nil {
return nil, err
}
opConfig := &op.Config{ opConfig := &op.Config{
DefaultLogoutRedirectURI: defaultLogoutRedirectURI, DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
CodeMethodS256: config.CodeMethodS256, CodeMethodS256: config.CodeMethodS256,
@@ -178,7 +174,6 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []
AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT, AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT,
GrantTypeRefreshToken: config.GrantTypeRefreshToken, GrantTypeRefreshToken: config.GrantTypeRefreshToken,
RequestObjectSupported: config.RequestObjectSupported, RequestObjectSupported: config.RequestObjectSupported,
SupportedUILocales: supportedLanguages,
DeviceAuthorization: config.DeviceAuth.toOPConfig(), DeviceAuthorization: config.DeviceAuth.toOPConfig(),
} }
if cryptoLength := len(cryptoKey); cryptoLength != 32 { if cryptoLength := len(cryptoKey); cryptoLength != 32 {
@@ -211,11 +206,3 @@ func newStorage(config Config, command *command.Commands, query *query.Queries,
func (o *OPStorage) Health(ctx context.Context) error { func (o *OPStorage) Health(ctx context.Context) error {
return o.repo.Health(ctx) return o.repo.Health(ctx)
} }
func getSupportedLanguages() ([]language.Tag, error) {
statikLoginFS, err := fs.NewWithNamespace("login")
if err != nil {
return nil, err
}
return i18n.SupportedLanguages(statikLoginFS)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/auth/repository" "github.com/zitadel/zitadel/internal/auth/repository"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
) )
@@ -26,6 +27,10 @@ type Server struct {
command *command.Commands command *command.Commands
keySet *keySetCache keySet *keySetCache
defaultLoginURL string
defaultLoginURLV2 string
defaultLogoutURLV2 string
fallbackLogger *slog.Logger fallbackLogger *slog.Logger
hashAlg crypto.HashAlgorithm hashAlg crypto.HashAlgorithm
signingKeyAlgorithm string signingKeyAlgorithm string
@@ -103,8 +108,15 @@ func (s *Server) Ready(ctx context.Context, r *op.Request[struct{}]) (_ *op.Resp
func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
restrictions, err := s.query.GetInstanceRestrictions(ctx)
return op.NewResponse(s.createDiscoveryConfig(ctx)), nil if err != nil {
return nil, err
}
allowedLanguages := restrictions.AllowedLanguages
if len(allowedLanguages) == 0 {
allowedLanguages = i18n.SupportedLanguages()
}
return op.NewResponse(s.createDiscoveryConfig(ctx, allowedLanguages)), nil
} }
func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) {
@@ -135,13 +147,6 @@ func (s *Server) DeviceAuthorization(ctx context.Context, r *op.ClientRequest[oi
return s.LegacyServer.DeviceAuthorization(ctx, r) return s.LegacyServer.DeviceAuthorization(ctx, r)
} }
func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return s.LegacyServer.VerifyClient(ctx, r)
}
func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.AccessTokenRequest]) (_ *op.Response, err error) { func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.AccessTokenRequest]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@@ -205,7 +210,7 @@ func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRe
return s.LegacyServer.EndSession(ctx, r) return s.LegacyServer.EndSession(ctx, r)
} }
func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfiguration { func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales oidc.Locales) *oidc.DiscoveryConfiguration {
issuer := op.IssuerFromContext(ctx) issuer := op.IssuerFromContext(ctx)
return &oidc.DiscoveryConfiguration{ return &oidc.DiscoveryConfiguration{
Issuer: issuer, Issuer: issuer,
@@ -231,7 +236,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfi
RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()), RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()),
ClaimsSupported: op.SupportedClaims(s.Provider()), ClaimsSupported: op.SupportedClaims(s.Provider()),
CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()), CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()),
UILocalesSupported: s.Provider().SupportedUILocales(), UILocalesSupported: supportedUILocales,
RequestParameterSupported: s.Provider().RequestObjectSupported(), RequestParameterSupported: s.Provider().RequestObjectSupported(),
} }
} }

View File

@@ -17,6 +17,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
supportedUILocales []language.Tag
} }
tests := []struct { tests := []struct {
name string name string
@@ -36,7 +37,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
AuthMethodPrivateKeyJWT: true, AuthMethodPrivateKeyJWT: true,
GrantTypeRefreshToken: true, GrantTypeRefreshToken: true,
RequestObjectSupported: true, RequestObjectSupported: true,
SupportedUILocales: []language.Tag{language.English, language.German},
}, },
nil, nil,
) )
@@ -57,6 +57,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
}, },
args{ args{
ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"), ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"),
supportedUILocales: []language.Tag{language.English, language.German},
}, },
&oidc.DiscoveryConfiguration{ &oidc.DiscoveryConfiguration{
Issuer: "https://issuer.com", Issuer: "https://issuer.com",
@@ -113,7 +114,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
LegacyServer: tt.fields.LegacyServer, LegacyServer: tt.fields.LegacyServer,
signingKeyAlgorithm: tt.fields.signingKeyAlgorithm, signingKeyAlgorithm: tt.fields.signingKeyAlgorithm,
} }
assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx), "createDiscoveryConfig(%v)", tt.args.ctx) assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx, tt.args.supportedUILocales), "createDiscoveryConfig(%v)", tt.args.ctx)
}) })
} }
} }

View File

@@ -36,13 +36,13 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
} }
func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string var errType, errMessage string
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errType, errMessage = l.getErrorMessage(r, err)
} }
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := passwordData{ data := passwordData{
baseData: l.getBaseData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errID, errMessage), baseData: l.getBaseData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage),
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
} }
policy := l.getPasswordComplexityPolicy(r, authReq.UserOrgID) policy := l.getPasswordComplexityPolicy(r, authReq.UserOrgID)
@@ -65,8 +65,7 @@ func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, aut
} }
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
var errType, errMessage string
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage) data := l.getUserData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", "", "")
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil)
} }

View File

@@ -28,13 +28,13 @@ func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request,
logging.WithError(err).Error() logging.WithError(err).Error()
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
translator := l.getTranslator(r.Context(), nil) translator := l.getTranslator(r.Context(), nil)
data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil)
} }
func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) { func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) {
translator := l.getTranslator(r.Context(), authReq)
data := &struct { data := &struct {
baseData baseData
AuthRequestID string AuthRequestID string
@@ -42,14 +42,13 @@ func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, a
ClientID string ClientID string
Scopes []string Scopes []string
}{ }{
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""), baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
AuthRequestID: authReq.ID, AuthRequestID: authReq.ID,
Username: authReq.UserName, Username: authReq.UserName,
ClientID: authReq.ApplicationID, ClientID: authReq.ApplicationID,
Scopes: scopes, Scopes: scopes,
} }
translator := l.getTranslator(r.Context(), authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil)
} }
@@ -60,14 +59,13 @@ const (
// renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied. // renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied.
func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) { func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) {
translator := l.getTranslator(r.Context(), authReq)
data := &struct { data := &struct {
baseData baseData
Message string Message string
}{ }{
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""), baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
} }
translator := l.getTranslator(r.Context(), authReq)
switch action { switch action {
case deviceAuthAllowed: case deviceAuthAllowed:
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil) data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)

View File

@@ -549,7 +549,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := externalNotFoundOptionData{ data := externalNotFoundOptionData{
baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage), baseData: l.getBaseData(r, authReq, translator, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
externalNotFoundOptionFormData: externalNotFoundOptionFormData{ externalNotFoundOptionFormData: externalNotFoundOptionFormData{
externalRegisterFormData: externalRegisterFormData{ externalRegisterFormData: externalRegisterFormData{
Email: human.EmailAddress, Email: human.EmailAddress,

View File

@@ -122,7 +122,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := initPasswordData{ data := initPasswordData{
baseData: l.getBaseData(r, authReq, "InitPassword.Title", "InitPassword.Description", errID, errMessage), baseData: l.getBaseData(r, authReq, translator, "InitPassword.Title", "InitPassword.Description", errID, errMessage),
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
UserID: userID, UserID: userID,
Code: code, Code: code,
@@ -153,8 +153,8 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
} }
func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
data := l.getUserData(r, authReq, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
if authReq == nil { if authReq == nil {
l.customTexts(r.Context(), translator, orgID) l.customTexts(r.Context(), translator, orgID)
} }

View File

@@ -118,7 +118,7 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := initUserData{ data := initUserData{
baseData: l.getBaseData(r, authReq, "InitUser.Title", "InitUser.Description", errID, errMessage), baseData: l.getBaseData(r, authReq, translator, "InitUser.Title", "InitUser.Description", errID, errMessage),
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
UserID: userID, UserID: userID,
Code: code, Code: code,
@@ -155,8 +155,8 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
} }
func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
data := l.getUserData(r, authReq, "InitUserDone.Title", "InitUserDone.Description", "", "")
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", "", "")
if authReq == nil { if authReq == nil {
l.customTexts(r.Context(), translator, orgID) l.customTexts(r.Context(), translator, orgID)
} }

View File

@@ -35,8 +35,9 @@ func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
temp := l.renderer.Templates[tmplLDAPLogin] temp := l.renderer.Templates[tmplLDAPLogin]
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage) translator := l.getTranslator(r.Context(), authReq)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil) data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, translator, temp, data, nil)
} }
func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) { func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) {

View File

@@ -19,6 +19,7 @@ func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domai
func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errType, errMessage string var errType, errMessage string
data := l.getUserData(r, authReq, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage) translator := l.getTranslator(r.Context(), authReq)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLinkUsersDone], data, nil) data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil)
} }

View File

@@ -2,15 +2,12 @@ package login
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/rakyll/statik/fs"
"github.com/zitadel/zitadel/feature" "github.com/zitadel/zitadel/feature"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http" http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -93,17 +90,12 @@ func CreateLogin(config Config,
userCodeAlg: userCodeAlg, userCodeAlg: userCodeAlg,
featureCheck: featureCheck, featureCheck: featureCheck,
} }
statikFS, err := fs.NewWithNamespace("login")
if err != nil {
return nil, fmt.Errorf("unable to create filesystem: %w", err)
}
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler()) csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache) cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler) security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler) login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName) login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName)
login.parser = form.NewParser() login.parser = form.NewParser()
return login, nil return login, nil
} }

View File

@@ -99,7 +99,8 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom
l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID)
return return
} }
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage) translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
funcs := map[string]interface{}{ funcs := map[string]interface{}{
"hasUsernamePasswordLogin": func() bool { "hasUsernamePasswordLogin": func() bool {
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword
@@ -111,7 +112,7 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowRegister return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowRegister
}, },
} }
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLogin], data, funcs) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogin], data, funcs)
} }
func singleIDPAllowed(authReq *domain.AuthRequest) bool { func singleIDPAllowed(authReq *domain.AuthRequest) bool {

View File

@@ -41,8 +41,9 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
translator := l.getTranslator(r.Context(), authReq)
data := loginSuccessData{ data := loginSuccessData{
userData: l.getUserData(r, authReq, "LoginSuccess.Title", "", errID, errMessage), userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", errID, errMessage),
} }
if authReq != nil { if authReq != nil {
data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq) data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq)
@@ -51,7 +52,7 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
return return
} }
} }
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLoginSuccess], data, nil)
} }
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {

View File

@@ -13,6 +13,7 @@ func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) {
} }
func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) { func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) {
data := l.getUserData(r, nil, "LogoutDone.Title", "LogoutDone.Description", "", "") translator := l.getTranslator(r.Context(), nil)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), nil), l.renderer.Templates[tmplLogoutDone], data, nil) data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", "", "")
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogoutDone], data, nil)
} }

View File

@@ -95,7 +95,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{ data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage), baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
UserID: userID, UserID: userID,
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
} }
@@ -111,7 +111,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{ data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""), baseData: l.getBaseData(r, authReq, translator, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""),
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
} }
if authReq == nil { if authReq == nil {

View File

@@ -16,7 +16,7 @@ type mfaInitDoneData struct {
func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) { func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) {
var errType, errMessage string var errType, errMessage string
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage) data.baseData = l.getBaseData(r, authReq, translator, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage)
data.profileData = l.getProfileData(authReq) data.profileData = l.getProfileData(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil)
} }

View File

@@ -57,10 +57,11 @@ func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authRe
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.profileData = l.getProfileData(authReq) data.profileData = l.getProfileData(authReq)
data.MFAType = domain.MFATypeOTPSMS data.MFAType = domain.MFATypeOTPSMS
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFASMSInit], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFASMSInit], data, nil)
} }
// handleRegisterSMSCheck handles form submissions of the SMS registration. // handleRegisterSMSCheck handles form submissions of the SMS registration.

View File

@@ -29,14 +29,15 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe
if u2f != nil { if u2f != nil {
credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData) credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData)
} }
translator := l.getTranslator(r.Context(), authReq)
data := &u2fInitData{ data := &u2fInitData{
webAuthNData: webAuthNData{ webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage), userData: l.getUserData(r, authReq, translator, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage),
CredentialCreationData: credentialData, CredentialCreationData: credentialData,
}, },
MFAType: domain.MFATypeU2F, MFAType: domain.MFATypeU2F,
} }
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFAU2FInit], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAU2FInit], data, nil)
} }
func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) { func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) {

View File

@@ -71,7 +71,7 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.profileData = l.getProfileData(authReq) data.profileData = l.getProfileData(authReq)
if data.MFAType == domain.MFATypeTOTP { if data.MFAType == domain.MFATypeTOTP {
code, err := generateQrCode(data.totpData.Url) code, err := generateQrCode(data.totpData.Url)

View File

@@ -56,7 +56,7 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq
} }
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := mfaData{ data := mfaData{
baseData: l.getBaseData(r, authReq, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage), baseData: l.getBaseData(r, authReq, translator, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage),
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
} }

View File

@@ -66,12 +66,12 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request,
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
data := l.getUserData(r, authReq, "", "", errID, errMessage) translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "", "", errID, errMessage)
if verificationStep == nil { if verificationStep == nil {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
} }
translator := l.getTranslator(r.Context(), authReq)
switch selectedProvider { switch selectedProvider {
case domain.MFATypeU2F: case domain.MFATypeU2F:

View File

@@ -61,12 +61,13 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
translator := l.getTranslator(r.Context(), authReq)
data := &mfaOTPData{ data := &mfaOTPData{
userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
MFAProviders: removeSelectedProviderFromList(providers, selectedProvider), MFAProviders: removeSelectedProviderFromList(providers, selectedProvider),
SelectedProvider: selectedProvider, SelectedProvider: selectedProvider,
} }
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplOTPVerification], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplOTPVerification], data, nil)
} }
// handleOTPVerificationCheck handles form submissions of the OTP verification. // handleOTPVerificationCheck handles form submissions of the OTP verification.

View File

@@ -37,15 +37,16 @@ func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, au
if webAuthNLogin != nil { if webAuthNLogin != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData) credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
} }
translator := l.getTranslator(r.Context(), authReq)
data := &mfaU2FData{ data := &mfaU2FData{
webAuthNData: webAuthNData{ webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
CredentialCreationData: credentialData, CredentialCreationData: credentialData,
}, },
MFAProviders: providers, MFAProviders: providers,
SelectedProvider: -1, SelectedProvider: -1,
} }
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplU2FVerification], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplU2FVerification], data, nil)
} }
func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) { func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) {

View File

@@ -19,7 +19,8 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
data := l.getUserData(r, authReq, "Password.Title", "Password.Description", errID, errMessage) translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Password.Title", "Password.Description", errID, errMessage)
funcs := map[string]interface{}{ funcs := map[string]interface{}{
"showPasswordReset": func() bool { "showPasswordReset": func() bool {
if authReq.LoginPolicy != nil { if authReq.LoginPolicy != nil {
@@ -28,7 +29,7 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
return true return true
}, },
} }
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPassword], data, funcs) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPassword], data, funcs)
} }
func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) { func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {

View File

@@ -48,6 +48,7 @@ func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request,
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
data := l.getUserData(r, authReq, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage) translator := l.getTranslator(r.Context(), authReq)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordResetDone], data, nil) data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordResetDone], data, nil)
} }

View File

@@ -36,14 +36,15 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re
if passwordSet && authReq.LoginPolicy != nil { if passwordSet && authReq.LoginPolicy != nil {
passwordSet = authReq.LoginPolicy.AllowUsernamePassword passwordSet = authReq.LoginPolicy.AllowUsernamePassword
} }
translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessData{ data := &passwordlessData{
webAuthNData{ webAuthNData{
userData: l.getUserData(r, authReq, "Passwordless.Title", "Passwordless.Description", errID, errMessage), userData: l.getUserData(r, authReq, translator, "Passwordless.Title", "Passwordless.Description", errID, errMessage),
CredentialCreationData: credentialData, CredentialCreationData: credentialData,
}, },
passwordSet, passwordSet,
} }
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordlessVerification], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessVerification], data, nil)
} }
func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) { func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) {

View File

@@ -31,10 +31,9 @@ func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request,
if err != nil { if err != nil {
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
data := &passwordlessPromptData{
userData: l.getUserData(r, authReq, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage),
}
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessPromptData{
userData: l.getUserData(r, authReq, translator, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage),
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil)
} }

View File

@@ -99,11 +99,10 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
if webAuthNToken != nil { if webAuthNToken != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData) credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData)
} }
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessRegistrationData{ data := &passwordlessRegistrationData{
webAuthNData{ webAuthNData{
userData: l.getUserData(r, authReq, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage), userData: l.getUserData(r, authReq, translator, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage),
CredentialCreationData: credentialData, CredentialCreationData: credentialData,
}, },
code, code,
@@ -117,8 +116,6 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false) policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false)
logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy") logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy")
data.LabelPolicy = labelPolicyToDomain(policy) data.LabelPolicy = labelPolicyToDomain(policy)
translator, err = l.renderer.NewTranslator(r.Context())
if err == nil { if err == nil {
texts, err := l.authRepo.GetLoginText(r.Context(), orgID) texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts") logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts")
@@ -193,9 +190,8 @@ func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *htt
errID, errMessage = l.getErrorMessage(r, err) errID, errMessage = l.getErrorMessage(r, err)
} }
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := passwordlessRegistrationDoneDate{ data := passwordlessRegistrationDoneDate{
userData: l.getUserData(r, authReq, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage), userData: l.getUserData(r, authReq, translator, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage),
HideNextButton: authReq == nil, HideNextButton: authReq == nil,
} }
if authReq == nil { if authReq == nil {

View File

@@ -96,7 +96,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
l.renderRegister(w, r, authRequest, data, err) l.renderRegister(w, r, authRequest, data, err)
return return
} }
user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil { if err != nil {
l.renderRegister(w, r, authRequest, data, err) l.renderRegister(w, r, authRequest, data, err)
@@ -160,7 +159,7 @@ func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authReque
} }
data := registerData{ data := registerData{
baseData: l.getBaseData(r, authRequest, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage), baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage),
registerFormData: *formData, registerFormData: *formData,
} }

View File

@@ -54,7 +54,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut
} }
translator := l.getTranslator(r.Context(), authReq) translator := l.getTranslator(r.Context(), authReq)
data := registerOptionData{ data := registerOptionData{
baseData: l.getBaseData(r, authReq, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage), baseData: l.getBaseData(r, authReq, translator, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage),
} }
funcs := map[string]interface{}{ funcs := map[string]interface{}{
"hasRegistration": func() bool { "hasRegistration": func() bool {

View File

@@ -1,7 +1,6 @@
package login package login
import ( import (
"context"
"net/http" "net/http"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
@@ -39,8 +38,12 @@ type registerOrgData struct {
} }
func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) { func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context()) restrictions, err := l.query.GetInstanceRestrictions(r.Context())
if disallowed || err != nil { if err != nil {
l.renderError(w, r, nil, err)
return
}
if restrictions.DisallowPublicOrgRegistration {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
@@ -54,8 +57,12 @@ func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
} }
func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) { func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context()) restrictions, err := l.query.GetInstanceRestrictions(r.Context())
if disallowed || err != nil { if err != nil {
l.renderError(w, r, nil, err)
return
}
if restrictions.DisallowPublicOrgRegistration {
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
return return
} }
@@ -99,7 +106,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
} }
translator := l.getTranslator(r.Context(), authRequest) translator := l.getTranslator(r.Context(), authRequest)
data := registerOrgData{ data := registerOrgData{
baseData: l.getBaseData(r, authRequest, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage), baseData: l.getBaseData(r, authRequest, translator, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage),
registerOrgFormData: *formData, registerOrgFormData: *formData,
} }
pwPolicy := l.getPasswordComplexityPolicy(r, "0") pwPolicy := l.getPasswordComplexityPolicy(r, "0")
@@ -130,11 +137,6 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil)
} }
func (l *Login) publicOrgRegistrationIsDisallowed(ctx context.Context) (bool, error) {
restrictions, err := l.query.GetInstanceRestrictions(ctx)
return restrictions.DisallowPublicOrgRegistration, err
}
func (d registerOrgFormData) toUserDomain() *domain.Human { func (d registerOrgFormData) toUserDomain() *domain.Human {
if d.Username == "" { if d.Username == "" {
d.Username = string(d.Email) d.Username = string(d.Email)

View File

@@ -39,7 +39,7 @@ type LanguageData struct {
Lang string Lang string
} }
func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage static.Storage, cookieName string) *Renderer { func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName string) *Renderer {
r := &Renderer{ r := &Renderer{
pathPrefix: pathPrefix, pathPrefix: pathPrefix,
staticStorage: staticStorage, staticStorage: staticStorage,
@@ -238,7 +238,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
} }
var err error var err error
r.Renderer, err = renderer.NewRenderer( r.Renderer, err = renderer.NewRenderer(
staticDir,
tmplMapping, funcs, tmplMapping, funcs,
cookieName, cookieName,
) )
@@ -343,13 +342,14 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth
_, msg = l.getErrorMessage(r, err) _, msg = l.getErrorMessage(r, err)
} }
data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg) translator := l.getTranslator(r.Context(), authReq)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplError], data, nil) data := l.getBaseData(r, authReq, translator, "Errors.Internal", "", "Internal", msg)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil)
} }
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData { func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData {
userData := userData{ userData := userData{
baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, errType, errMessage), baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, errType, errMessage),
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
} }
if authReq != nil && authReq.LinkingUsers != nil { if authReq != nil && authReq.LinkingUsers != nil {
@@ -358,9 +358,7 @@ func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI
return userData return userData
} }
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData { func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
translator := l.getTranslator(r.Context(), authReq)
title := "" title := ""
if titleI18nKey != "" { if titleI18nKey != "" {
title = translator.LocalizeWithoutArgs(titleI18nKey) title = translator.LocalizeWithoutArgs(titleI18nKey)
@@ -418,7 +416,11 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI
} }
func (l *Login) getTranslator(ctx context.Context, authReq *domain.AuthRequest) *i18n.Translator { func (l *Login) getTranslator(ctx context.Context, authReq *domain.AuthRequest) *i18n.Translator {
translator, err := l.renderer.NewTranslator(ctx) restrictions, err := l.query.GetInstanceRestrictions(ctx)
if err != nil {
logging.OnError(err).Warn("cannot load instance restrictions to retrieve allowed languages for creating the translator")
}
translator, err := l.renderer.NewTranslator(ctx, restrictions.AllowedLanguages)
logging.OnError(err).Warn("cannot load translator") logging.OnError(err).Warn("cannot load translator")
if authReq != nil { if authReq != nil {
l.addLoginTranslations(translator, authReq.DefaultTranslations) l.addLoginTranslations(translator, authReq.DefaultTranslations)

View File

@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/assets" "github.com/zitadel/zitadel/internal/api/assets"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/i18n"
) )
type dynamicResourceData struct { type dynamicResourceData struct {
@@ -15,8 +16,8 @@ type dynamicResourceData struct {
FileName string `schema:"filename"` FileName string `schema:"filename"`
} }
func (l *Login) handleResources(staticDir http.FileSystem) http.Handler { func (l *Login) handleResources() http.Handler {
return http.FileServer(staticDir) return http.FileServer(i18n.LoadFilesystem(i18n.LOGIN))
} }
func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) { func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {

Some files were not shown because too many files have changed in this diff Show More