From 432603e313897f86168e53eaf29f2cb87ed985d8 Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Thu, 18 Dec 2025 15:08:44 +0300 Subject: [PATCH 01/16] init 'issue/hello-keycloak' --- keycloak/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 keycloak/README.md diff --git a/keycloak/README.md b/keycloak/README.md new file mode 100644 index 0000000..e69de29 From 7e8783783b789ae1e49ad9f35eeca14e1fc8756c Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Thu, 18 Dec 2025 19:49:14 +0300 Subject: [PATCH 02/16] add some notes taken during learn process --- keycloak/README.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/keycloak/README.md b/keycloak/README.md index e69de29..607238e 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -0,0 +1,82 @@ +# Keycloak + +Buradaki dokumantasyon keycloak'un docker ile kullanımına yöneliktir. + +## Configuration + +4 yöntem ile keycloak konfigüre edilebilir. + +1. Command-line parameters +1. Environment variables +1. Options defined in the conf/keycloak.conf file, or in a user-created configuration file. +1. Sensitive options defined in a user-created Java KeyStore file. + +Örneklere configuration file ile devam edeceğiz. + +config dosyasına configler = şeklinde eklenir. + +default config dosya path conf/keycloak.conf tir + +ayrıca You can use placeholders to resolve an environment specific value from environment variables inside the keycloak.conf file by using the ${ENV_VAR} syntax: + +db-url-host=${MY_DB_HOST} + +In case the environment variable cannot be resolved, you can specify a fallback value. Use a : (colon) as shown here before mydb: + +db-url-host=${MY_DB_HOST:mydb} + +> :Info: +> +> Special Characters +> +> \ character functions as an escape character +> $ characters when they appear to define an expression or are repeated + +### db + +db ekleme için config dosyasına db-url-host=mykeycloakdb eklenir + +### Quarkus framework + +Keycloak yapılandırmasında eksik olan belirli bir davranış veya yetenek için, +altta yatan Quarkus çerçevesinin özelliklerini kullanabilirsiniz. +detay için https://www.keycloak.org/server/configuration#_format_for_raw_quarkus_properties + +## Modes + +Keycloak development ve production modlarında çalışabilir. Default modu development modudur. +bir kaç özellik bu modda disabledır. detay için https://www.keycloak.org/server/configuration#_starting_keycloak_in_development_mode + +### Production mode + +Production mode da başlatmak için bazı ayarlamaya ihtiyaç duyuyor. bunlar + +- HTTP is disabled as transport layer security (HTTPS) is essential +- Hostname configuration is expected +- HTTPS/TLS configuration is expected + +## UI + +{base-url}/admin adresine giderek keycloak yönetim arayüzüne erişilebilir. +Burada realm ve user ekleme silme gibi bir çok işlem yapılabiliyor. + +## API + +Ayakta olan keycloak servisini api ile yönetebiliyoruz. bu api lere bir örnek olarak +POST /admin/realms/{realm}/logout-all +verilebilir. Apiler {base-url}/admin/ ile başlar. Bütün api leri görmek için +https://www.keycloak.org/docs-api/latest/rest-api/index.html adresine bakılabilir. + +## Realms + +Keycloak'ta alanlar üzerinden yönetim yapılır. Her alan kendi kullanıcıları tutar. +Başlangıçta bir adet "master" alanı vardır. Önerilen master alanını sadece keycloak'ı yönetmek için kullanmaktır. + +## Docker + +docker ile kullanımda local için pek olasada prod ortamlarında iyi memory +ayırlamaları yapmak gerekiyor. + +Memory hesaplamaları için +https://www.keycloak.org/high-availability/single-cluster/concepts-memory-and-cpu-sizing#single-cluster-single-site-calculation +bakılabilir. \ No newline at end of file From d2f8f72ef17587dd7a81790e9c9f91a31b5135c9 Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Sat, 20 Dec 2025 16:24:26 +0300 Subject: [PATCH 03/16] add simple project, compose and test http file --- .gitignore | 3 ++ keycloak/README.md | 20 +++++++++ keycloak/WeatherService/Dockerfile | 21 +++++++++ keycloak/WeatherService/Program.cs | 43 +++++++++++++++++++ .../Properties/launchSettings.json | 23 ++++++++++ keycloak/WeatherService/WeatherResponse.cs | 8 ++++ keycloak/WeatherService/WeatherService.csproj | 13 ++++++ .../appsettings.Development.json | 8 ++++ keycloak/WeatherService/appsettings.json | 13 ++++++ keycloak/compose.yml | 23 ++++++++++ keycloak/test-api.http | 27 ++++++++++++ learn-devops.sln | 29 +++++++++++++ 12 files changed, 231 insertions(+) create mode 100644 .gitignore create mode 100644 keycloak/WeatherService/Dockerfile create mode 100644 keycloak/WeatherService/Program.cs create mode 100644 keycloak/WeatherService/Properties/launchSettings.json create mode 100644 keycloak/WeatherService/WeatherResponse.cs create mode 100644 keycloak/WeatherService/WeatherService.csproj create mode 100644 keycloak/WeatherService/appsettings.Development.json create mode 100644 keycloak/WeatherService/appsettings.json create mode 100644 keycloak/compose.yml create mode 100644 keycloak/test-api.http create mode 100644 learn-devops.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03c9b93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +.vs/ diff --git a/keycloak/README.md b/keycloak/README.md index 607238e..9ba7424 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -32,6 +32,13 @@ db-url-host=${MY_DB_HOST:mydb} > \ character functions as an escape character > $ characters when they appear to define an expression or are repeated +bütün configler için https://www.keycloak.org/server/all-config?f=build + +> :Info: +> +> run sırasında bazı realm ayarlarına izin verilmez ama spi-admin--allowed-system-variables gibi flaglar ile +> izin verdirilebilir. + ### db db ekleme için config dosyasına db-url-host=mykeycloakdb eklenir @@ -55,6 +62,19 @@ Production mode da başlatmak için bazı ayarlamaya ihtiyaç duyuyor. bunlar - Hostname configuration is expected - HTTPS/TLS configuration is expected +## Optimizations + +Dockerda kullanmayı planladığımız için ayağa kaldırma süresini optimize etmemiz gerekiyor. +Bunun için Keycloak tarafından önerilen bazı optimizasyonlar var. + +1. Normal build yap +2. --optimized flag ile başlat + +--optimized flag zaten build alındı sen önceki build i kullan demek. + +eğer --optimized flag ile başlatılan uygulamaya build config verilirse ve hali +hazırda pre-build te bu verilmişse run sırasında verilen ignore edilir + ## UI {base-url}/admin adresine giderek keycloak yönetim arayüzüne erişilebilir. diff --git a/keycloak/WeatherService/Dockerfile b/keycloak/WeatherService/Dockerfile new file mode 100644 index 0000000..bec0640 --- /dev/null +++ b/keycloak/WeatherService/Dockerfile @@ -0,0 +1,21 @@ +# AI Generated + +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy csproj and restore dependencies +COPY WeatherService.csproj . +RUN dotnet restore + +# Copy everything else and build +COPY . . +RUN dotnet publish -c Release -o /app/publish + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +WORKDIR /app +COPY --from=build /app/publish . + +EXPOSE 8080 +ENTRYPOINT ["dotnet", "WeatherService.dll"] diff --git a/keycloak/WeatherService/Program.cs b/keycloak/WeatherService/Program.cs new file mode 100644 index 0000000..cbd915e --- /dev/null +++ b/keycloak/WeatherService/Program.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = builder.Configuration["Jwt:Authority"]; + options.Audience = builder.Configuration["Jwt:Audience"]; + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true + }; + }); + +builder.Services.AddAuthorization(); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/", () => "Weather Service is running!"); + +app.MapGet("/weather", () => +{ + var random = new Random(); + var weather = new WeatherResponse + { + Temperature = random.Next(-10, 40), + HasRain = random.Next(100) < 30, + Description = random.Next(100) < 30 ? "Rainy" : "Sunny", + City = "İstanbul", + Date = DateTime.Now + }; + return Results.Ok(weather); +}).RequireAuthorization(); + +app.Run(); \ No newline at end of file diff --git a/keycloak/WeatherService/Properties/launchSettings.json b/keycloak/WeatherService/Properties/launchSettings.json new file mode 100644 index 0000000..3bd7c37 --- /dev/null +++ b/keycloak/WeatherService/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5275", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7132;http://localhost:5275", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/keycloak/WeatherService/WeatherResponse.cs b/keycloak/WeatherService/WeatherResponse.cs new file mode 100644 index 0000000..5b5989c --- /dev/null +++ b/keycloak/WeatherService/WeatherResponse.cs @@ -0,0 +1,8 @@ +public record WeatherResponse +{ + public int Temperature { get; init; } + public bool HasRain { get; init; } + public string Description { get; init; } = string.Empty; + public string City { get; init; } = string.Empty; + public DateTime Date { get; init; } +} diff --git a/keycloak/WeatherService/WeatherService.csproj b/keycloak/WeatherService/WeatherService.csproj new file mode 100644 index 0000000..7941d0b --- /dev/null +++ b/keycloak/WeatherService/WeatherService.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/keycloak/WeatherService/appsettings.Development.json b/keycloak/WeatherService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/keycloak/WeatherService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/keycloak/WeatherService/appsettings.json b/keycloak/WeatherService/appsettings.json new file mode 100644 index 0000000..4b400e8 --- /dev/null +++ b/keycloak/WeatherService/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Jwt": { + "Authority": "http://keycloak:8080/realms/master", + "Audience": "weather-api" + } +} diff --git a/keycloak/compose.yml b/keycloak/compose.yml new file mode 100644 index 0000000..8c131e4 --- /dev/null +++ b/keycloak/compose.yml @@ -0,0 +1,23 @@ +services: + keycloak: + image: quay.io/keycloak/keycloak:latest + container_name: keycloak + environment: + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + ports: + - "8080:8080" + command: start-dev + + weather-service: + build: + context: ./WeatherService + dockerfile: Dockerfile + container_name: weather-service + ports: + - "5000:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_HTTP_PORTS=8080 + depends_on: + - keycloak \ No newline at end of file diff --git a/keycloak/test-api.http b/keycloak/test-api.http new file mode 100644 index 0000000..376ec08 --- /dev/null +++ b/keycloak/test-api.http @@ -0,0 +1,27 @@ +@keycloakUrl = http://localhost:8080 +@weatherServiceUrl = http://localhost:5000 +@realm = master +@clientId = admin-cli +@username = admin +@password = admin + +### + +# @name getToken +POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/token +Content-Type: application/x-www-form-urlencoded + +grant_type=password&client_id={{clientId}}&username={{username}}&password={{password}} + +### + +GET {{weatherServiceUrl}}/ + +### + +GET {{weatherServiceUrl}}/weather + +### + +GET {{weatherServiceUrl}}/weather +Authorization: Bearer {{getToken.response.body.access_token}} diff --git a/learn-devops.sln b/learn-devops.sln new file mode 100644 index 0000000..1c5bbc4 --- /dev/null +++ b/learn-devops.sln @@ -0,0 +1,29 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "keycloak", "keycloak", "{4B5D3AB7-A3DA-2315-2A6C-49C09F1422D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeatherService", "keycloak\WeatherService\WeatherService.csproj", "{71188429-1219-F9F0-2787-95DF822AD803}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {71188429-1219-F9F0-2787-95DF822AD803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71188429-1219-F9F0-2787-95DF822AD803}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71188429-1219-F9F0-2787-95DF822AD803}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71188429-1219-F9F0-2787-95DF822AD803}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {71188429-1219-F9F0-2787-95DF822AD803} = {4B5D3AB7-A3DA-2315-2A6C-49C09F1422D9} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {224833B9-0692-448C-812F-BA26384CFE31} + EndGlobalSection +EndGlobal From e7181095429158d4b1a05a3bc15377118ebcc6b4 Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Sat, 20 Dec 2025 18:56:59 +0300 Subject: [PATCH 04/16] improve example --- keycloak/README.md | 3 ++ keycloak/WeatherService/Program.cs | 14 +++++---- keycloak/WeatherService/WeatherResponse.cs | 11 ++++---- keycloak/compose.yml | 4 ++- keycloak/realm-config.json | 33 ++++++++++++++++++++++ keycloak/test-api.http | 32 ++++++++++++++++----- 6 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 keycloak/realm-config.json diff --git a/keycloak/README.md b/keycloak/README.md index 9ba7424..f6cc933 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -87,6 +87,9 @@ POST /admin/realms/{realm}/logout-all verilebilir. Apiler {base-url}/admin/ ile başlar. Bütün api leri görmek için https://www.keycloak.org/docs-api/latest/rest-api/index.html adresine bakılabilir. +çalışan keycloak servisine GET http://localhost:8080/realms/master/.well-known/openid-configuration isteği atılırsa +endpointleri görebiliriz. + ## Realms Keycloak'ta alanlar üzerinden yönetim yapılır. Her alan kendi kullanıcıları tutar. diff --git a/keycloak/WeatherService/Program.cs b/keycloak/WeatherService/Program.cs index cbd915e..345d01c 100644 --- a/keycloak/WeatherService/Program.cs +++ b/keycloak/WeatherService/Program.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; +using WeatherService; var builder = WebApplication.CreateBuilder(args); @@ -13,7 +14,13 @@ { ValidateIssuer = true, ValidateAudience = true, - ValidateLifetime = true + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + ValidIssuers = + [ + "http://localhost:8080/realms/master", + "http://keycloak:8080/realms/master" + ] }; }); @@ -32,10 +39,7 @@ var weather = new WeatherResponse { Temperature = random.Next(-10, 40), - HasRain = random.Next(100) < 30, - Description = random.Next(100) < 30 ? "Rainy" : "Sunny", - City = "İstanbul", - Date = DateTime.Now + Sky = random.Next(100) < 30 ? "Rainy" : "Sunny" }; return Results.Ok(weather); }).RequireAuthorization(); diff --git a/keycloak/WeatherService/WeatherResponse.cs b/keycloak/WeatherService/WeatherResponse.cs index 5b5989c..5337d77 100644 --- a/keycloak/WeatherService/WeatherResponse.cs +++ b/keycloak/WeatherService/WeatherResponse.cs @@ -1,8 +1,7 @@ +namespace WeatherService; + public record WeatherResponse { - public int Temperature { get; init; } - public bool HasRain { get; init; } - public string Description { get; init; } = string.Empty; - public string City { get; init; } = string.Empty; - public DateTime Date { get; init; } -} +public int Temperature { get; init; } +public string? Sky { get; init; } +} \ No newline at end of file diff --git a/keycloak/compose.yml b/keycloak/compose.yml index 8c131e4..c44ca49 100644 --- a/keycloak/compose.yml +++ b/keycloak/compose.yml @@ -7,7 +7,9 @@ services: - KEYCLOAK_ADMIN_PASSWORD=admin ports: - "8080:8080" - command: start-dev + volumes: + - ./realm-config.json:/opt/keycloak/data/import/realm-config.json:ro + command: start-dev --import-realm weather-service: build: diff --git a/keycloak/realm-config.json b/keycloak/realm-config.json new file mode 100644 index 0000000..fd83c51 --- /dev/null +++ b/keycloak/realm-config.json @@ -0,0 +1,33 @@ +{ + "realm": "master", + "enabled": true, + "accessTokenLifespan": 30, + "ssoSessionIdleTimeout": 300, + "ssoSessionMaxLifespan": 600, + "clients": [ + { + "clientId": "weather-api", + "enabled": true, + "publicClient": false, + "secret": "weather-secret", + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "standardFlowEnabled": false, + "attributes": { + "access.token.lifespan": "30" + }, + "protocolMappers": [ + { + "name": "weather-api-audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "config": { + "included.client.audience": "weather-api", + "id.token.claim": "false", + "access.token.claim": "true" + } + } + ] + } + ] +} diff --git a/keycloak/test-api.http b/keycloak/test-api.http index 376ec08..f4f0015 100644 --- a/keycloak/test-api.http +++ b/keycloak/test-api.http @@ -1,27 +1,45 @@ @keycloakUrl = http://localhost:8080 @weatherServiceUrl = http://localhost:5000 @realm = master -@clientId = admin-cli -@username = admin -@password = admin ### - -# @name getToken +# Get Token +# @name login POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded -grant_type=password&client_id={{clientId}}&username={{username}}&password={{password}} +grant_type=client_credentials&client_id=weather-api&client_secret=weather-secret + +### + +# Logout +POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/revoke +Content-Type: application/x-www-form-urlencoded + +client_id=weather-api&client_secret=weather-secret&token={{login.response.body.access_token}} ### +# Status GET {{weatherServiceUrl}}/ ### +# Without Token GET {{weatherServiceUrl}}/weather ### +# With Token GET {{weatherServiceUrl}}/weather -Authorization: Bearer {{getToken.response.body.access_token}} +Authorization: Bearer {{login.response.body.access_token}} + +### + +# OpenID Configuration +GET {{keycloakUrl}}/realms/{{realm}}/.well-known/openid-configuration + +### + +# Public Keys for JWT +GET {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/certs From 756867dff43aceba3b2d46cf7973ec9a0420ae6a Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Mon, 22 Dec 2025 10:41:25 +0300 Subject: [PATCH 05/16] rename realm and use dockerfile to setup keycloak --- keycloak/README.md | 7 ++++ keycloak/WeatherService/Program.cs | 4 +-- keycloak/WeatherService/appsettings.json | 2 +- keycloak/compose.yml | 44 ++++++++++++++++++++--- keycloak/keycloak/Dockerfile | 7 ++++ keycloak/keycloak/keycloak.conf | 14 ++++++++ keycloak/{ => keycloak}/realm-config.json | 2 +- keycloak/test-api.http | 4 +-- 8 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 keycloak/keycloak/Dockerfile create mode 100644 keycloak/keycloak/keycloak.conf rename keycloak/{ => keycloak}/realm-config.json (96%) diff --git a/keycloak/README.md b/keycloak/README.md index f6cc933..818aaba 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -49,6 +49,13 @@ Keycloak yapılandırmasında eksik olan belirli bir davranış veya yetenek iç altta yatan Quarkus çerçevesinin özelliklerini kullanabilirsiniz. detay için https://www.keycloak.org/server/configuration#_format_for_raw_quarkus_properties +## Import realms + +İlk kurulumda master realm otomatik oluşturuluyor. Master realm kullanılması önerilmiyor. +Kullanılacak realm uygulama ayağa kalktıktan sonra ui dan yada bir istekle ile oluşturulabilir. +Alternatif olarak önceden hazırlanmış realm json dosyası import edilebilir. json dosyasını +import etmek için image da /opt/keycloak/data/import/ altına json dosyası olarak kopyalamak yeterlidir. + ## Modes Keycloak development ve production modlarında çalışabilir. Default modu development modudur. diff --git a/keycloak/WeatherService/Program.cs b/keycloak/WeatherService/Program.cs index 345d01c..80242dd 100644 --- a/keycloak/WeatherService/Program.cs +++ b/keycloak/WeatherService/Program.cs @@ -18,8 +18,8 @@ ClockSkew = TimeSpan.Zero, ValidIssuers = [ - "http://localhost:8080/realms/master", - "http://keycloak:8080/realms/master" + "http://localhost:8080/realms/test-realm", + "http://keycloak:8080/realms/test-realm" ] }; }); diff --git a/keycloak/WeatherService/appsettings.json b/keycloak/WeatherService/appsettings.json index 4b400e8..ae3f9c2 100644 --- a/keycloak/WeatherService/appsettings.json +++ b/keycloak/WeatherService/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "Jwt": { - "Authority": "http://keycloak:8080/realms/master", + "Authority": "http://keycloak:8080/realms/test-realm", "Audience": "weather-api" } } diff --git a/keycloak/compose.yml b/keycloak/compose.yml index c44ca49..d9d9ddf 100644 --- a/keycloak/compose.yml +++ b/keycloak/compose.yml @@ -1,15 +1,40 @@ services: + postgres: + image: postgres:16 + container_name: keycloak-db + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: keycloak + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "keycloak"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - keycloak-network + keycloak: - image: quay.io/keycloak/keycloak:latest + build: + context: ./keycloak + dockerfile: Dockerfile container_name: keycloak environment: - KEYCLOAK_ADMIN=admin - KEYCLOAK_ADMIN_PASSWORD=admin + - KC_DB_HOST=postgres + - KC_DB_NAME=keycloak + - KC_DB_USER=keycloak + - KC_DB_PASSWORD=keycloak ports: - "8080:8080" - volumes: - - ./realm-config.json:/opt/keycloak/data/import/realm-config.json:ro - command: start-dev --import-realm + depends_on: + postgres: + condition: service_healthy + networks: + - keycloak-network weather-service: build: @@ -22,4 +47,13 @@ services: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_HTTP_PORTS=8080 depends_on: - - keycloak \ No newline at end of file + - keycloak + networks: + - keycloak-network + +volumes: + postgres_data: + +networks: + keycloak-network: + driver: bridge \ No newline at end of file diff --git a/keycloak/keycloak/Dockerfile b/keycloak/keycloak/Dockerfile new file mode 100644 index 0000000..d2312bd --- /dev/null +++ b/keycloak/keycloak/Dockerfile @@ -0,0 +1,7 @@ +FROM quay.io/keycloak/keycloak:latest + +COPY keycloak.conf /opt/keycloak/conf/keycloak.conf +COPY realm-config.json /opt/keycloak/data/import/ + +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] +CMD ["start-dev", "--import-realm"] diff --git a/keycloak/keycloak/keycloak.conf b/keycloak/keycloak/keycloak.conf new file mode 100644 index 0000000..8df4254 --- /dev/null +++ b/keycloak/keycloak/keycloak.conf @@ -0,0 +1,14 @@ +# Database +db=postgres +db-url-host=${KC_DB_HOST:postgres} +db-url-database=${KC_DB_NAME:keycloak} +db-username=${KC_DB_USER:keycloak} +db-password=${KC_DB_PASSWORD:keycloak} + +# HTTP +http-enabled=true +http-port=8080 +hostname-strict=false + +# Health +health-enabled=true diff --git a/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json similarity index 96% rename from keycloak/realm-config.json rename to keycloak/keycloak/realm-config.json index fd83c51..22fc6ce 100644 --- a/keycloak/realm-config.json +++ b/keycloak/keycloak/realm-config.json @@ -1,5 +1,5 @@ { - "realm": "master", + "realm": "test-realm", "enabled": true, "accessTokenLifespan": 30, "ssoSessionIdleTimeout": 300, diff --git a/keycloak/test-api.http b/keycloak/test-api.http index f4f0015..4cf4ef4 100644 --- a/keycloak/test-api.http +++ b/keycloak/test-api.http @@ -1,6 +1,6 @@ @keycloakUrl = http://localhost:8080 @weatherServiceUrl = http://localhost:5000 -@realm = master +@realm = test-realm ### # Get Token @@ -13,7 +13,7 @@ grant_type=client_credentials&client_id=weather-api&client_secret=weather-secret ### # Logout -POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/revoke +POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/logout Content-Type: application/x-www-form-urlencoded client_id=weather-api&client_secret=weather-secret&token={{login.response.body.access_token}} From 43a3ffdc9f58c6f43911ee0c5fa6f601adab347a Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Mon, 22 Dec 2025 11:33:17 +0300 Subject: [PATCH 06/16] diversify the sample --- keycloak/keycloak/realm-config.json | 79 +++++++++++++++++++++++++++++ keycloak/test-api.http | 54 ++++++++++++++++++-- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json index 22fc6ce..f0769bf 100644 --- a/keycloak/keycloak/realm-config.json +++ b/keycloak/keycloak/realm-config.json @@ -4,6 +4,22 @@ "accessTokenLifespan": 30, "ssoSessionIdleTimeout": 300, "ssoSessionMaxLifespan": 600, + "roles": { + "realm": [ + { + "name": "admin", + "description": "Administrator role" + }, + { + "name": "user", + "description": "Standard user role" + }, + { + "name": "viewer", + "description": "Read-only access" + } + ] + }, "clients": [ { "clientId": "weather-api", @@ -26,8 +42,71 @@ "id.token.claim": "false", "access.token.claim": "true" } + }, + { + "name": "realm-roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "config": { + "claim.name": "roles", + "jsonType.label": "String", + "multivalued": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } } ] } + ], + "users": [ + { + "username": "admin", + "enabled": true, + "emailVerified": true, + "firstName": "Admin", + "lastName": "User", + "email": "admin@example.com", + "credentials": [ + { + "type": "password", + "value": "admin123", + "temporary": false + } + ], + "realmRoles": ["admin", "user"] + }, + { + "username": "testuser", + "enabled": true, + "emailVerified": true, + "firstName": "Test", + "lastName": "User", + "email": "testuser@example.com", + "credentials": [ + { + "type": "password", + "value": "testpass", + "temporary": false + } + ], + "realmRoles": ["user"] + }, + { + "username": "viewer", + "enabled": true, + "emailVerified": true, + "firstName": "Viewer", + "lastName": "User", + "email": "viewer@example.com", + "credentials": [ + { + "type": "password", + "value": "viewer123", + "temporary": false + } + ], + "realmRoles": ["viewer"] + } ] } diff --git a/keycloak/test-api.http b/keycloak/test-api.http index 4cf4ef4..8eb0f54 100644 --- a/keycloak/test-api.http +++ b/keycloak/test-api.http @@ -1,14 +1,62 @@ @keycloakUrl = http://localhost:8080 @weatherServiceUrl = http://localhost:5000 @realm = test-realm +@username = testuser +@password = testpass ### -# Get Token + +# @name adminToken +POST {{keycloakUrl}}/realms/master/protocol/openid-connect/token +Content-Type: application/x-www-form-urlencoded + +grant_type=password&client_id=admin-cli&username=admin&password=admin + +### + +# User Ekle +POST {{keycloakUrl}}/admin/realms/{{realm}}/users +Authorization: Bearer {{adminToken.response.body.access_token}} +Content-Type: application/json + +{ + "username": "{{username}}", + "enabled": true, + "emailVerified": true, + "firstName": "Test", + "lastName": "User", + "email": "testuser@example.com", + "credentials": [ + { + "type": "password", + "value": "{{password}}", + "temporary": false + } + ], + "realmRoles": ["user"], + "attributes": { + "department": ["engineering"] + } +} + +### + +# User Listele +GET {{keycloakUrl}}/admin/realms/{{realm}}/users +Authorization: Bearer {{adminToken.response.body.access_token}} + +### + +GET {{keycloakUrl}}/admin/realms/{{realm}}/users?username={{username}} +Authorization: Bearer {{adminToken.response.body.access_token}} + +### + # @name login POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded -grant_type=client_credentials&client_id=weather-api&client_secret=weather-secret +grant_type=password&client_id=weather-api&client_secret=weather-secret&username={{username}}&password={{password}} ### @@ -16,7 +64,7 @@ grant_type=client_credentials&client_id=weather-api&client_secret=weather-secret POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/logout Content-Type: application/x-www-form-urlencoded -client_id=weather-api&client_secret=weather-secret&token={{login.response.body.access_token}} +client_id=weather-api&client_secret=weather-secret&refresh_token={{login.response.body.refresh_token}} ### From 8f77f708d4327a9a9ed1f13ee8b4688498451aea Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Mon, 22 Dec 2025 16:43:15 +0300 Subject: [PATCH 07/16] edit realm config --- keycloak/README.md | 10 ++++++ keycloak/compose.yml | 2 ++ keycloak/keycloak/realm-config.json | 55 +++++++++++++++++------------ keycloak/test-api.http | 10 ++---- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/keycloak/README.md b/keycloak/README.md index 818aaba..a3cbc80 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -102,6 +102,16 @@ endpointleri görebiliriz. Keycloak'ta alanlar üzerinden yönetim yapılır. Her alan kendi kullanıcıları tutar. Başlangıçta bir adet "master" alanı vardır. Önerilen master alanını sadece keycloak'ı yönetmek için kullanmaktır. +## Token + +tokenda claimler eklerken Protocol Mapper kullanılıyor. Örneğin bir client için +login olunduğunda gelen token da default olarak audience alanı olmuyor. Bunu eklemek +için(json ile import edildiği varsayılarak) client'ın protocolMappers alanına + +"protocolMapper": "oidc-audience-mapper", + +objesi eklemek gerekiyor. realm.config.json dosyasına bakınız + ## Docker docker ile kullanımda local için pek olasada prod ortamlarında iyi memory diff --git a/keycloak/compose.yml b/keycloak/compose.yml index d9d9ddf..b634a23 100644 --- a/keycloak/compose.yml +++ b/keycloak/compose.yml @@ -6,6 +6,8 @@ services: POSTGRES_DB: keycloak POSTGRES_USER: keycloak POSTGRES_PASSWORD: keycloak + ports: + - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json index f0769bf..2dfbf45 100644 --- a/keycloak/keycloak/realm-config.json +++ b/keycloak/keycloak/realm-config.json @@ -13,10 +13,6 @@ { "name": "user", "description": "Standard user role" - }, - { - "name": "viewer", - "description": "Read-only access" } ] }, @@ -34,7 +30,7 @@ }, "protocolMappers": [ { - "name": "weather-api-audience", + "name": "audience", "protocol": "openid-connect", "protocolMapper": "oidc-audience-mapper", "config": { @@ -52,8 +48,31 @@ "jsonType.label": "String", "multivalued": "true", "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" + "access.token.claim": "true" + } + }, + { + "name": "department", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "claim.name": "department", + "user.attribute": "department", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "config": { + "claim.name": "username", + "user.attribute": "username", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true" } } ] @@ -67,6 +86,9 @@ "firstName": "Admin", "lastName": "User", "email": "admin@example.com", + "attributes": { + "department": ["management"] + }, "credentials": [ { "type": "password", @@ -83,6 +105,9 @@ "firstName": "Test", "lastName": "User", "email": "testuser@example.com", + "attributes": { + "department": ["engineering"] + }, "credentials": [ { "type": "password", @@ -91,22 +116,6 @@ } ], "realmRoles": ["user"] - }, - { - "username": "viewer", - "enabled": true, - "emailVerified": true, - "firstName": "Viewer", - "lastName": "User", - "email": "viewer@example.com", - "credentials": [ - { - "type": "password", - "value": "viewer123", - "temporary": false - } - ], - "realmRoles": ["viewer"] } ] } diff --git a/keycloak/test-api.http b/keycloak/test-api.http index 8eb0f54..37df6fc 100644 --- a/keycloak/test-api.http +++ b/keycloak/test-api.http @@ -14,7 +14,7 @@ grant_type=password&client_id=admin-cli&username=admin&password=admin ### -# User Ekle +# Add User POST {{keycloakUrl}}/admin/realms/{{realm}}/users Authorization: Bearer {{adminToken.response.body.access_token}} Content-Type: application/json @@ -41,12 +41,13 @@ Content-Type: application/json ### -# User Listele +# User List GET {{keycloakUrl}}/admin/realms/{{realm}}/users Authorization: Bearer {{adminToken.response.body.access_token}} ### +# Find User GET {{keycloakUrl}}/admin/realms/{{realm}}/users?username={{username}} Authorization: Bearer {{adminToken.response.body.access_token}} @@ -68,11 +69,6 @@ client_id=weather-api&client_secret=weather-secret&refresh_token={{login.respons ### -# Status -GET {{weatherServiceUrl}}/ - -### - # Without Token GET {{weatherServiceUrl}}/weather From 2b136cd43bc6dfaf5438d22ae15067d1be66018f Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Mon, 22 Dec 2025 17:18:16 +0300 Subject: [PATCH 08/16] add multivalued field to mapper --- keycloak/keycloak/realm-config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json index 2dfbf45..2a37537 100644 --- a/keycloak/keycloak/realm-config.json +++ b/keycloak/keycloak/realm-config.json @@ -59,6 +59,7 @@ "claim.name": "department", "user.attribute": "department", "jsonType.label": "String", + "multivalued": "true", "id.token.claim": "true", "access.token.claim": "true" } From c07bccbe6ecd5e635c70495d33c3a4f40f140842 Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Mon, 22 Dec 2025 20:13:11 +0300 Subject: [PATCH 09/16] update document and example --- README.md | 52 ++--------- docker/README.md | 47 ++++++++++ dockerfile => docker/dockerfile | 8 +- keycloak/README.md | 130 +++++++++++++--------------- keycloak/compose.yml | 3 - keycloak/keycloak/keycloak.conf | 6 +- keycloak/keycloak/realm-config.json | 6 -- 7 files changed, 121 insertions(+), 131 deletions(-) create mode 100644 docker/README.md rename dockerfile => docker/dockerfile (97%) diff --git a/README.md b/README.md index 1718ce1..69623d2 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,13 @@ -# Docker Image +# Learn DevOps -Docker images are created using dockerfiles. A sample dockerfile, from which -you can create docker images, is given at the root of the repository. This -sample dockerfile will be used in the demo section. +This is a hands-on learning repository where we explore and experiment with +DevOps tools, technologies, and best practices. -## Create image with dockerfile +Each topic is organized in its own directory with dedicated documentation and +examples. -To build a docker image, run `docker build . ` at the directory your -dockerfile is in. This searches for a dockerfile in the run directory and -creates a docker image from found dockerfile. You can give name to docker image -with `-t {image-name}`, `docker build -t {image-name} . ` +## What We've Learned So Far -## Run image in container - -`docker run`, creates an instance of docker image and runs it in a container. -Adding args at the end of the run command will run those arguments, thus you -can pass commands as arguments. - -`docker run {image-name} args` - -## Persisting Data - -To bind a host directory to container, use `docker run` with -`-v {absolutepath}:{containerpath}`. - -`docker run -v {absolutepath}:{containerpath} {image-name} args` - -Changes made in this folder by the container persists, you can observe these -changes in host folder. To get the output of the container in host folder, set -the output as the container folder, which is bound to host folder. - -`docker run --rm -v ${PWD}/host-output:/container-output args {-output} /container-output` - -## Reaching Localhost from Container - -To reach your machine's localhost from the container, when giving the url use -`host.docker.internal` instead of localhost. - -`http://host.docker.internal:{port}`. - -## Demo - -Dockerfile at the root of this repository wraps -[web-ping](https://github.com/SeriaWei/Ping). You can create a docker -image from this dockerfile and use this cli in your container by passing -`web-ping Web.Ping --host https://github.com/` command as argument. \ No newline at end of file +- [Docker](docker/README.md): Docker images, Dockerfiles, and container basics +- [Keycloak](keycloak/README.md): Identity management, JWT authentication, + Docker Compose setup diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..1718ce1 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,47 @@ +# Docker Image + +Docker images are created using dockerfiles. A sample dockerfile, from which +you can create docker images, is given at the root of the repository. This +sample dockerfile will be used in the demo section. + +## Create image with dockerfile + +To build a docker image, run `docker build . ` at the directory your +dockerfile is in. This searches for a dockerfile in the run directory and +creates a docker image from found dockerfile. You can give name to docker image +with `-t {image-name}`, `docker build -t {image-name} . ` + +## Run image in container + +`docker run`, creates an instance of docker image and runs it in a container. +Adding args at the end of the run command will run those arguments, thus you +can pass commands as arguments. + +`docker run {image-name} args` + +## Persisting Data + +To bind a host directory to container, use `docker run` with +`-v {absolutepath}:{containerpath}`. + +`docker run -v {absolutepath}:{containerpath} {image-name} args` + +Changes made in this folder by the container persists, you can observe these +changes in host folder. To get the output of the container in host folder, set +the output as the container folder, which is bound to host folder. + +`docker run --rm -v ${PWD}/host-output:/container-output args {-output} /container-output` + +## Reaching Localhost from Container + +To reach your machine's localhost from the container, when giving the url use +`host.docker.internal` instead of localhost. + +`http://host.docker.internal:{port}`. + +## Demo + +Dockerfile at the root of this repository wraps +[web-ping](https://github.com/SeriaWei/Ping). You can create a docker +image from this dockerfile and use this cli in your container by passing +`web-ping Web.Ping --host https://github.com/` command as argument. \ No newline at end of file diff --git a/dockerfile b/docker/dockerfile similarity index 97% rename from dockerfile rename to docker/dockerfile index c7311ee..f009e49 100644 --- a/dockerfile +++ b/docker/dockerfile @@ -1,5 +1,5 @@ -# Parent image -FROM mcr.microsoft.com/dotnet/sdk:6.0 - -RUN dotnet tool install --global Web.Ping --version 0.0.4 +# Parent image +FROM mcr.microsoft.com/dotnet/sdk:6.0 + +RUN dotnet tool install --global Web.Ping --version 0.0.4 ENV PATH="/root/.dotnet/tools:${PATH}" \ No newline at end of file diff --git a/keycloak/README.md b/keycloak/README.md index a3cbc80..d490f60 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -1,122 +1,108 @@ # Keycloak -Buradaki dokumantasyon keycloak'un docker ile kullanımına yöneliktir. +This documentation covers using Keycloak with Docker in this repo. ## Configuration -4 yöntem ile keycloak konfigüre edilebilir. +Keycloak can be configured in four ways: 1. Command-line parameters -1. Environment variables -1. Options defined in the conf/keycloak.conf file, or in a user-created configuration file. -1. Sensitive options defined in a user-created Java KeyStore file. +2. Environment variables +3. Options in `conf/keycloak.conf` (or a user-provided config file) +4. Sensitive options in a Java KeyStore -Örneklere configuration file ile devam edeceğiz. +We use the config file approach. Options follow the format +`=`. -config dosyasına configler = şeklinde eklenir. +- Default config path: `conf/keycloak.conf` +- Environment placeholders: `${ENV_VAR}` with optional fallback + `${ENV_VAR:default}` +- Escaping: `\` escapes characters; `$` is special in expressions -default config dosya path conf/keycloak.conf tir +All available options: [All Configs] -ayrıca You can use placeholders to resolve an environment specific value from environment variables inside the keycloak.conf file by using the ${ENV_VAR} syntax: +Note: Some realm settings are restricted at runtime; enabling flags (e.g., +`spi-admin-allowed-system-variables`) should be used cautiously. -db-url-host=${MY_DB_HOST} +### Database -In case the environment variable cannot be resolved, you can specify a fallback value. Use a : (colon) as shown here before mydb: +You can configure the database via the config file, environment variables, or +CLI flags. Precedence: CLI > environment > config file. -db-url-host=${MY_DB_HOST:mydb} +Using `keycloak.conf`: -> :Info: -> -> Special Characters -> -> \ character functions as an escape character -> $ characters when they appear to define an expression or are repeated - -bütün configler için https://www.keycloak.org/server/all-config?f=build - -> :Info: -> -> run sırasında bazı realm ayarlarına izin verilmez ama spi-admin--allowed-system-variables gibi flaglar ile -> izin verdirilebilir. - -### db - -db ekleme için config dosyasına db-url-host=mykeycloakdb eklenir +``` +db-url-host=mykeycloakdb +``` ### Quarkus framework -Keycloak yapılandırmasında eksik olan belirli bir davranış veya yetenek için, -altta yatan Quarkus çerçevesinin özelliklerini kullanabilirsiniz. -detay için https://www.keycloak.org/server/configuration#_format_for_raw_quarkus_properties +For gaps in Keycloak options, you can fall back to raw Quarkus properties: +[Quarkus Properties] ## Import realms -İlk kurulumda master realm otomatik oluşturuluyor. Master realm kullanılması önerilmiyor. -Kullanılacak realm uygulama ayağa kalktıktan sonra ui dan yada bir istekle ile oluşturulabilir. -Alternatif olarak önceden hazırlanmış realm json dosyası import edilebilir. json dosyasını -import etmek için image da /opt/keycloak/data/import/ altına json dosyası olarak kopyalamak yeterlidir. +Keycloak creates a `master` realm by default; avoid using it for applications. +You can create realms via the UI or Admin API, or import a prebuilt JSON by +copying it into the image under `/opt/keycloak/data/import/` and starting with +realm import enabled. ## Modes -Keycloak development ve production modlarında çalışabilir. Default modu development modudur. -bir kaç özellik bu modda disabledır. detay için https://www.keycloak.org/server/configuration#_starting_keycloak_in_development_mode +Keycloak runs in development (default) and production modes. Some features +differ: [Dev Mode] ### Production mode -Production mode da başlatmak için bazı ayarlamaya ihtiyaç duyuyor. bunlar +Production requires additional setup: -- HTTP is disabled as transport layer security (HTTPS) is essential -- Hostname configuration is expected -- HTTPS/TLS configuration is expected +- HTTP disabled; HTTPS (TLS) required +- Hostname configuration required +- HTTPS/TLS configuration required ## Optimizations -Dockerda kullanmayı planladığımız için ayağa kaldırma süresini optimize etmemiz gerekiyor. -Bunun için Keycloak tarafından önerilen bazı optimizasyonlar var. +For faster startup in containers, use the recommended flow: -1. Normal build yap -2. --optimized flag ile başlat +1. Build once normally +2. Start with `--optimized` to reuse the build ---optimized flag zaten build alındı sen önceki build i kullan demek. - -eğer --optimized flag ile başlatılan uygulamaya build config verilirse ve hali -hazırda pre-build te bu verilmişse run sırasında verilen ignore edilir +If runtime build config conflicts with a pre-build, the pre-built assets take +precedence. ## UI -{base-url}/admin adresine giderek keycloak yönetim arayüzüne erişilebilir. -Burada realm ve user ekleme silme gibi bir çok işlem yapılabiliyor. +Access the Admin Console at `{base-url}/admin` to manage realms, users, and +settings. ## API -Ayakta olan keycloak servisini api ile yönetebiliyoruz. bu api lere bir örnek olarak -POST /admin/realms/{realm}/logout-all -verilebilir. Apiler {base-url}/admin/ ile başlar. Bütün api leri görmek için -https://www.keycloak.org/docs-api/latest/rest-api/index.html adresine bakılabilir. +Manage Keycloak via the Admin REST API under `{base-url}/admin`. +Example: `POST /admin/realms/{realm}/logout-all` +API reference: [API Reference] -çalışan keycloak servisine GET http://localhost:8080/realms/master/.well-known/openid-configuration isteği atılırsa -endpointleri görebiliriz. +OpenID Connect discovery: [OIDC Discovery] ## Realms -Keycloak'ta alanlar üzerinden yönetim yapılır. Her alan kendi kullanıcıları tutar. -Başlangıçta bir adet "master" alanı vardır. Önerilen master alanını sadece keycloak'ı yönetmek için kullanmaktır. +Realms isolate users and configuration. The `master` realm exists by default and +should be used only for administering Keycloak. ## Token -tokenda claimler eklerken Protocol Mapper kullanılıyor. Örneğin bir client için -login olunduğunda gelen token da default olarak audience alanı olmuyor. Bunu eklemek -için(json ile import edildiği varsayılarak) client'ın protocolMappers alanına - -"protocolMapper": "oidc-audience-mapper", - -objesi eklemek gerekiyor. realm.config.json dosyasına bakınız +Use Protocol Mappers to add claims to tokens. For example, adding an audience +claim uses the `oidc-audience-mapper`. See `keycloak/realm-config.json` in this +repo for basic examples. ## Docker -docker ile kullanımda local için pek olasada prod ortamlarında iyi memory -ayırlamaları yapmak gerekiyor. +For production, size memory appropriately. Guidance: [Sizing Guide] + +## References -Memory hesaplamaları için -https://www.keycloak.org/high-availability/single-cluster/concepts-memory-and-cpu-sizing#single-cluster-single-site-calculation -bakılabilir. \ No newline at end of file +[All Configs]: https://www.keycloak.org/server/all-config?f=build +[Quarkus Properties]: https://www.keycloak.org/server/configuration#_format_for_raw_quarkus_properties +[Dev Mode]: https://www.keycloak.org/server/configuration#_starting_keycloak_in_development_mode +[API Reference]: https://www.keycloak.org/docs-api/latest/rest-api/index.html +[OIDC Discovery]: http://localhost:8080/realms/master/.well-known/openid-configuration +[Sizing Guide]: https://www.keycloak.org/high-availability/single-cluster/concepts-memory-and-cpu-sizing#single-cluster-single-site-calculation \ No newline at end of file diff --git a/keycloak/compose.yml b/keycloak/compose.yml index b634a23..912e1c4 100644 --- a/keycloak/compose.yml +++ b/keycloak/compose.yml @@ -26,9 +26,6 @@ services: environment: - KEYCLOAK_ADMIN=admin - KEYCLOAK_ADMIN_PASSWORD=admin - - KC_DB_HOST=postgres - - KC_DB_NAME=keycloak - - KC_DB_USER=keycloak - KC_DB_PASSWORD=keycloak ports: - "8080:8080" diff --git a/keycloak/keycloak/keycloak.conf b/keycloak/keycloak/keycloak.conf index 8df4254..5e3304f 100644 --- a/keycloak/keycloak/keycloak.conf +++ b/keycloak/keycloak/keycloak.conf @@ -1,8 +1,8 @@ # Database db=postgres -db-url-host=${KC_DB_HOST:postgres} -db-url-database=${KC_DB_NAME:keycloak} -db-username=${KC_DB_USER:keycloak} +db-url-host=postgres +db-url-database=keycloak +db-username=keycloak db-password=${KC_DB_PASSWORD:keycloak} # HTTP diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json index 2a37537..019eecc 100644 --- a/keycloak/keycloak/realm-config.json +++ b/keycloak/keycloak/realm-config.json @@ -87,9 +87,6 @@ "firstName": "Admin", "lastName": "User", "email": "admin@example.com", - "attributes": { - "department": ["management"] - }, "credentials": [ { "type": "password", @@ -106,9 +103,6 @@ "firstName": "Test", "lastName": "User", "email": "testuser@example.com", - "attributes": { - "department": ["engineering"] - }, "credentials": [ { "type": "password", From d279053bc6781ebcf5bb1454b57557b0fecb49f7 Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Tue, 23 Dec 2025 20:14:49 +0300 Subject: [PATCH 10/16] add cors and ui page --- keycloak/Ui/index.html | 46 +++++++++++++++++++++++++++++ keycloak/Ui/nginx.conf | 17 +++++++++++ keycloak/WeatherService/Program.cs | 12 ++++++++ keycloak/compose.yml | 18 +++++++++-- keycloak/keycloak/realm-config.json | 25 ---------------- keycloak/test-api.http | 2 +- 6 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 keycloak/Ui/index.html create mode 100644 keycloak/Ui/nginx.conf diff --git a/keycloak/Ui/index.html b/keycloak/Ui/index.html new file mode 100644 index 0000000..7407227 --- /dev/null +++ b/keycloak/Ui/index.html @@ -0,0 +1,46 @@ + + + + + Keycloak Flow Demo + + +

Weather

+ +

+
+  
+
+
diff --git a/keycloak/Ui/nginx.conf b/keycloak/Ui/nginx.conf
new file mode 100644
index 0000000..710645b
--- /dev/null
+++ b/keycloak/Ui/nginx.conf
@@ -0,0 +1,17 @@
+events {}
+http {
+  server {
+    listen 80;
+
+    root /usr/share/nginx/;
+
+    location / {
+      try_files $uri /index.html;
+    }
+
+    location /weather {
+      proxy_pass http://weather-service:8000/;
+      proxy_set_header Host $host;
+    }
+  }
+}
\ No newline at end of file
diff --git a/keycloak/WeatherService/Program.cs b/keycloak/WeatherService/Program.cs
index 80242dd..d7998ca 100644
--- a/keycloak/WeatherService/Program.cs
+++ b/keycloak/WeatherService/Program.cs
@@ -4,6 +4,16 @@
 
 var builder = WebApplication.CreateBuilder(args);
 
+builder.Services.AddCors(options =>
+{
+    options.AddPolicy("AllowAll", policy =>
+    {
+        policy.AllowAnyOrigin()
+              .AllowAnyMethod()
+              .AllowAnyHeader();
+    });
+});
+
 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
     .AddJwtBearer(options =>
     {
@@ -28,6 +38,8 @@
 
 var app = builder.Build();
 
+app.UseCors("AllowAll");
+
 app.UseAuthentication();
 app.UseAuthorization();
 
diff --git a/keycloak/compose.yml b/keycloak/compose.yml
index 912e1c4..34b6abb 100644
--- a/keycloak/compose.yml
+++ b/keycloak/compose.yml
@@ -41,15 +41,29 @@ services:
       dockerfile: Dockerfile
     container_name: weather-service
     ports:
-      - "5000:8080"
+      - "5000:8000"
     environment:
       - ASPNETCORE_ENVIRONMENT=Development
-      - ASPNETCORE_HTTP_PORTS=8080
+      - ASPNETCORE_HTTP_PORTS=8000
+      - ASPNETCORE_URLS=http://+:8000
     depends_on:
       - keycloak
     networks:
       - keycloak-network
 
+  nginx:
+    image: nginx:alpine
+    ports:
+      - "80:80"
+    volumes:
+      - ./Ui/nginx.conf:/etc/nginx/nginx.conf:ro
+      - ./Ui/index.html:/usr/share/nginx/index.html:ro
+    depends_on:
+      - weather-service
+      - keycloak
+    networks:
+      - keycloak-network
+
 volumes:
   postgres_data:
 
diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json
index 019eecc..d278be1 100644
--- a/keycloak/keycloak/realm-config.json
+++ b/keycloak/keycloak/realm-config.json
@@ -50,31 +50,6 @@
             "id.token.claim": "true",
             "access.token.claim": "true"
           }
-        },
-        {
-          "name": "department",
-          "protocol": "openid-connect",
-          "protocolMapper": "oidc-usermodel-attribute-mapper",
-          "config": {
-            "claim.name": "department",
-            "user.attribute": "department",
-            "jsonType.label": "String",
-            "multivalued": "true",
-            "id.token.claim": "true",
-            "access.token.claim": "true"
-          }
-        },
-        {
-          "name": "username",
-          "protocol": "openid-connect",
-          "protocolMapper": "oidc-usermodel-property-mapper",
-          "config": {
-            "claim.name": "username",
-            "user.attribute": "username",
-            "jsonType.label": "String",
-            "id.token.claim": "true",
-            "access.token.claim": "true"
-          }
         }
       ]
     }
diff --git a/keycloak/test-api.http b/keycloak/test-api.http
index 37df6fc..745c2ff 100644
--- a/keycloak/test-api.http
+++ b/keycloak/test-api.http
@@ -1,6 +1,6 @@
 @keycloakUrl = http://localhost:8080
 @weatherServiceUrl = http://localhost:5000
-@realm = test-realm
+@realm = test-realm2
 @username = testuser
 @password = testpass
 

From 72e86205a814f04c881319e28e71c3ec6587963c Mon Sep 17 00:00:00 2001
From: Sefer Mirza 
Date: Wed, 24 Dec 2025 19:29:29 +0300
Subject: [PATCH 11/16] improve example

---
 keycloak/README.md                  |  73 ++++++++++++++
 keycloak/Ui/index.html              | 149 +++++++++++++++++++++++-----
 keycloak/WeatherService/Program.cs  |   7 +-
 keycloak/keycloak/realm-config.json |   7 +-
 keycloak/test-api.http              |   6 +-
 5 files changed, 211 insertions(+), 31 deletions(-)

diff --git a/keycloak/README.md b/keycloak/README.md
index d490f60..e5b925b 100644
--- a/keycloak/README.md
+++ b/keycloak/README.md
@@ -94,6 +94,79 @@ Use Protocol Mappers to add claims to tokens. For example, adding an audience
 claim uses the `oidc-audience-mapper`. See `keycloak/realm-config.json` in this
 repo for basic examples.
 
+## Login and Redirect
+
+To start the login flow, send a GET request to:
+
+```
+GET http://localhost:8080/realms//protocol/openid-connect/auth
+```
+
+Query parameters:
+- `client_id`: The client ID (e.g. weather-api)
+- `response_type`: `code` (for Authorization Code Flow)
+- `scope`: `openid` (and any additional scopes)
+- `redirect_uri`: Where the user will be redirected after login
+  (e.g. http://localhost)
+- `prompt`: (optional) Controls whether the login screen is shown. Use
+  `prompt=none` to attempt silent authentication (no UI); if the user is not
+  already logged in, an error is returned instead of showing the login page.
+  This is useful for checking session status or implementing silent SSO.
+
+Example request:
+```url
+http://localhost:8080/realms/test-realm/protocol/openid-connect/auth
+  ?client_id=weather-api
+  &response_type=code
+  &scope=openid
+  &redirect_uri=http://localhost
+  &prompt=none
+```
+
+
+
+After successful login, Keycloak redirects to:
+```
+http://localhost/?code=AUTH_CODE&session_state=...&iss=...
+```
+
+To exchange the `code` for an access token, send a POST request to:
+```
+POST http://localhost:8080/realms//protocol/openid-connect/token
+Content-Type: application/x-www-form-urlencoded
+
+client_id=weather-api
+&grant_type=authorization_code
+&code=AUTH_CODE
+&redirect_uri=http://localhost
+```
+
+Sample response:
+```
+{
+  "access_token": "...",
+  "refresh_token": "...",
+  ...
+}
+```
+
+#### Redirect URI Settings
+
+For the redirect to work, the client must have:
+- `redirectUris`: Allowed redirect URIs (e.g. ["http://localhost/*"])
+- `webOrigins`: Allowed CORS origins (e.g. ["http://localhost"])
+- `standardFlowEnabled`: true (required for Authorization Code Flow)
+
+> [Info]
+>
+> If you want to obtain tokens directly from a frontend (SPA/JS) app, set
+> `publicClient: true` and do not send `client_secret` in the token request. For
+> confidential clients (`publicClient: false`), using the secret in the frontend
+> is insecure and required by Keycloak.
+
+These settings must be present in both JSON imports and in the Keycloak UI
+client configuration.
+
 ## Docker
 
 For production, size memory appropriately. Guidance: [Sizing Guide]
diff --git a/keycloak/Ui/index.html b/keycloak/Ui/index.html
index 7407227..1c19436 100644
--- a/keycloak/Ui/index.html
+++ b/keycloak/Ui/index.html
@@ -3,44 +3,147 @@
 
   
   Keycloak Flow Demo
+  
 
 
-  

Weather

+

Weather App

-

+  
Click "Load Weather" to start
- + \ No newline at end of file diff --git a/keycloak/WeatherService/Program.cs b/keycloak/WeatherService/Program.cs index d7998ca..9f5d4d0 100644 --- a/keycloak/WeatherService/Program.cs +++ b/keycloak/WeatherService/Program.cs @@ -9,8 +9,8 @@ options.AddPolicy("AllowAll", policy => { policy.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); + .AllowAnyMethod() + .AllowAnyHeader(); }); }); @@ -20,11 +20,13 @@ options.Authority = builder.Configuration["Jwt:Authority"]; options.Audience = builder.Configuration["Jwt:Audience"]; options.RequireHttpsMetadata = false; + options.MetadataAddress = $"{builder.Configuration["Jwt:Authority"]}/.well-known/openid-configuration"; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, + ValidateIssuerSigningKey = true, ClockSkew = TimeSpan.Zero, ValidIssuers = [ @@ -53,6 +55,7 @@ Temperature = random.Next(-10, 40), Sky = random.Next(100) < 30 ? "Rainy" : "Sunny" }; + return Results.Ok(weather); }).RequireAuthorization(); diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json index d278be1..d31253c 100644 --- a/keycloak/keycloak/realm-config.json +++ b/keycloak/keycloak/realm-config.json @@ -19,12 +19,13 @@ "clients": [ { "clientId": "weather-api", + "redirectUris": ["http://localhost/*"], + "webOrigins": ["http://localhost"], "enabled": true, - "publicClient": false, - "secret": "weather-secret", + "publicClient": true, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": true, - "standardFlowEnabled": false, + "standardFlowEnabled": true, "attributes": { "access.token.lifespan": "30" }, diff --git a/keycloak/test-api.http b/keycloak/test-api.http index 745c2ff..c0e8b21 100644 --- a/keycloak/test-api.http +++ b/keycloak/test-api.http @@ -1,6 +1,6 @@ @keycloakUrl = http://localhost:8080 @weatherServiceUrl = http://localhost:5000 -@realm = test-realm2 +@realm = test-realm @username = testuser @password = testpass @@ -57,7 +57,7 @@ Authorization: Bearer {{adminToken.response.body.access_token}} POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded -grant_type=password&client_id=weather-api&client_secret=weather-secret&username={{username}}&password={{password}} +grant_type=password&client_id=weather-api&username={{username}}&password={{password}} ### @@ -65,7 +65,7 @@ grant_type=password&client_id=weather-api&client_secret=weather-secret&username= POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/logout Content-Type: application/x-www-form-urlencoded -client_id=weather-api&client_secret=weather-secret&refresh_token={{login.response.body.refresh_token}} +client_id=weather-api&refresh_token={{login.response.body.refresh_token}} ### From 75b6217aebb76bcbfd35d86a5e0cde7da38e4278 Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Wed, 24 Dec 2025 19:40:31 +0300 Subject: [PATCH 12/16] indent --- keycloak/Ui/index.html | 222 ++++++++++++++++++++--------------------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/keycloak/Ui/index.html b/keycloak/Ui/index.html index 1c19436..6e82fdf 100644 --- a/keycloak/Ui/index.html +++ b/keycloak/Ui/index.html @@ -15,135 +15,135 @@

Weather App

Click "Load Weather" to start
\ No newline at end of file From 1f32ff1f6892dcb995a61935a7e776cdf1a82eae Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Wed, 24 Dec 2025 19:49:25 +0300 Subject: [PATCH 13/16] remove unnecessary config --- keycloak/keycloak/realm-config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json index d31253c..04e7c59 100644 --- a/keycloak/keycloak/realm-config.json +++ b/keycloak/keycloak/realm-config.json @@ -20,7 +20,6 @@ { "clientId": "weather-api", "redirectUris": ["http://localhost/*"], - "webOrigins": ["http://localhost"], "enabled": true, "publicClient": true, "directAccessGrantsEnabled": true, From 3addc509a5ccfe27b03d262febe74c135d4b3c1b Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Wed, 24 Dec 2025 20:14:29 +0300 Subject: [PATCH 14/16] edit document --- keycloak/README.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/keycloak/README.md b/keycloak/README.md index e5b925b..0a2bc4e 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -17,7 +17,6 @@ We use the config file approach. Options follow the format - Default config path: `conf/keycloak.conf` - Environment placeholders: `${ENV_VAR}` with optional fallback `${ENV_VAR:default}` -- Escaping: `\` escapes characters; `$` is special in expressions All available options: [All Configs] @@ -113,7 +112,7 @@ Query parameters: already logged in, an error is returned instead of showing the login page. This is useful for checking session status or implementing silent SSO. -Example request: +Request: ```url http://localhost:8080/realms/test-realm/protocol/openid-connect/auth ?client_id=weather-api @@ -123,8 +122,6 @@ http://localhost:8080/realms/test-realm/protocol/openid-connect/auth &prompt=none ``` - - After successful login, Keycloak redirects to: ``` http://localhost/?code=AUTH_CODE&session_state=...&iss=... @@ -141,15 +138,6 @@ client_id=weather-api &redirect_uri=http://localhost ``` -Sample response: -``` -{ - "access_token": "...", - "refresh_token": "...", - ... -} -``` - #### Redirect URI Settings For the redirect to work, the client must have: From df2e5c81d18ca251c6365ce23a9be8b8cd11114d Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Thu, 25 Dec 2025 19:18:59 +0300 Subject: [PATCH 15/16] get token on backend service from keycloak --- keycloak/Ui/index.html | 165 +++++++-------------- keycloak/WeatherService/JwtTokenBuilder.cs | 33 +++++ keycloak/WeatherService/KeycloakService.cs | 32 ++++ keycloak/WeatherService/LoginResponse.cs | 3 + keycloak/WeatherService/Program.cs | 47 +++--- keycloak/WeatherService/WeatherResponse.cs | 4 +- keycloak/WeatherService/appsettings.json | 12 +- keycloak/keycloak/Dockerfile | 3 +- keycloak/keycloak/realm-config.json | 91 ------------ keycloak/test-api.http | 89 ----------- 10 files changed, 161 insertions(+), 318 deletions(-) create mode 100644 keycloak/WeatherService/JwtTokenBuilder.cs create mode 100644 keycloak/WeatherService/KeycloakService.cs create mode 100644 keycloak/WeatherService/LoginResponse.cs delete mode 100644 keycloak/keycloak/realm-config.json delete mode 100644 keycloak/test-api.http diff --git a/keycloak/Ui/index.html b/keycloak/Ui/index.html index 6e82fdf..9c28179 100644 --- a/keycloak/Ui/index.html +++ b/keycloak/Ui/index.html @@ -15,135 +15,84 @@

Weather App

Click "Load Weather" to start
\ No newline at end of file diff --git a/keycloak/WeatherService/JwtTokenBuilder.cs b/keycloak/WeatherService/JwtTokenBuilder.cs new file mode 100644 index 0000000..86c6634 --- /dev/null +++ b/keycloak/WeatherService/JwtTokenBuilder.cs @@ -0,0 +1,33 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace WeatherService; + +public class JwtTokenBuilder(IConfiguration _configuration, TimeProvider _timeProvider) +{ + readonly int _defaultExpiresInMinutes = 20; + + string Key => _configuration.GetValue($"Authentication:Jwt:{nameof(Key)}", "WeatherService"); + string? Issuer => _configuration.GetValue($"Authentication:Jwt:{nameof(Issuer)}"); + string? Audience => _configuration.GetValue($"Authentication:Jwt:{nameof(Audience)}"); + + public string Build(List claims) + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Key)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var header = new JwtHeader(creds); + var payload = new JwtPayload( + issuer: Issuer, + audience: Audience, + claims: claims, + notBefore: null, + issuedAt: _timeProvider.GetUtcNow().DateTime, + expires: _timeProvider.GetUtcNow().AddMinutes(_defaultExpiresInMinutes).DateTime + ); + + return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(header, payload)); + } +} \ No newline at end of file diff --git a/keycloak/WeatherService/KeycloakService.cs b/keycloak/WeatherService/KeycloakService.cs new file mode 100644 index 0000000..325fe63 --- /dev/null +++ b/keycloak/WeatherService/KeycloakService.cs @@ -0,0 +1,32 @@ +using System.Text.Json; + +namespace WeatherService; + +public class KeycloakClient(IConfiguration _configuration, HttpClient _httpClient) +{ + private readonly HttpClient _httpClient = _httpClient; + private readonly string _tokenEndpoint = "http://localhost:8080/realms/test-realm/protocol/openid-connect/token"; + private readonly string _clientId = "weather-api"; + private readonly string _clientSecret = _configuration["Keycloak:ClientSecret"] ?? "clientSecret"; + + public async Task GetTokenByCodeAsync(string code) + { + var parameters = new Dictionary + { + { "grant_type", "authorization_code" }, + { "code", code }, + { "client_id", _clientId }, + { "client_secret", _clientSecret } + }; + + var content = new FormUrlEncodedContent(parameters); + var response = await _httpClient.PostAsync(_tokenEndpoint, content); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + var accessToken = doc.RootElement.GetProperty("access_token").GetString(); + + return accessToken; + } +} + diff --git a/keycloak/WeatherService/LoginResponse.cs b/keycloak/WeatherService/LoginResponse.cs new file mode 100644 index 0000000..97254f7 --- /dev/null +++ b/keycloak/WeatherService/LoginResponse.cs @@ -0,0 +1,3 @@ +namespace WeatherService; + +public record LoginResponse(string AccessToken); \ No newline at end of file diff --git a/keycloak/WeatherService/Program.cs b/keycloak/WeatherService/Program.cs index 9f5d4d0..d3ac9ec 100644 --- a/keycloak/WeatherService/Program.cs +++ b/keycloak/WeatherService/Program.cs @@ -1,5 +1,6 @@ +using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Mvc; using WeatherService; var builder = WebApplication.CreateBuilder(args); @@ -15,29 +16,14 @@ }); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.Authority = builder.Configuration["Jwt:Authority"]; - options.Audience = builder.Configuration["Jwt:Audience"]; - options.RequireHttpsMetadata = false; - options.MetadataAddress = $"{builder.Configuration["Jwt:Authority"]}/.well-known/openid-configuration"; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ClockSkew = TimeSpan.Zero, - ValidIssuers = - [ - "http://localhost:8080/realms/test-realm", - "http://keycloak:8080/realms/test-realm" - ] - }; - }); + .AddJwtBearer(); builder.Services.AddAuthorization(); +builder.Services.AddSingleton(); + +builder.Services.AddHttpClient(); + var app = builder.Build(); app.UseCors("AllowAll"); @@ -45,8 +31,6 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("/", () => "Weather Service is running!"); - app.MapGet("/weather", () => { var random = new Random(); @@ -59,4 +43,21 @@ return Results.Ok(weather); }).RequireAuthorization(); +app.MapPost("/login-by-code", async ([FromServices] JwtTokenBuilder tokenBuilder, [FromServices] KeycloakClient keycloakClient, [FromBody] string code) => +{ + var accessToken = await keycloakClient.GetTokenByCodeAsync(code); + var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); + var jwt = handler.ReadJwtToken(accessToken); + var username = jwt.Claims.FirstOrDefault(c => c.Type == "username")?.Value; + + List claims = []; + if (!string.IsNullOrEmpty(username)) + { + claims.Add(new Claim(ClaimTypes.Name, username)); + } + + string token = tokenBuilder.Build(claims); + return Results.Ok(new LoginResponse(token)); +}).AllowAnonymous(); + app.Run(); \ No newline at end of file diff --git a/keycloak/WeatherService/WeatherResponse.cs b/keycloak/WeatherService/WeatherResponse.cs index 5337d77..264df5c 100644 --- a/keycloak/WeatherService/WeatherResponse.cs +++ b/keycloak/WeatherService/WeatherResponse.cs @@ -2,6 +2,6 @@ namespace WeatherService; public record WeatherResponse { -public int Temperature { get; init; } -public string? Sky { get; init; } + public int Temperature { get; init; } + public string? Sky { get; init; } } \ No newline at end of file diff --git a/keycloak/WeatherService/appsettings.json b/keycloak/WeatherService/appsettings.json index ae3f9c2..0c23753 100644 --- a/keycloak/WeatherService/appsettings.json +++ b/keycloak/WeatherService/appsettings.json @@ -6,8 +6,14 @@ } }, "AllowedHosts": "*", - "Jwt": { - "Authority": "http://keycloak:8080/realms/test-realm", - "Audience": "weather-api" + "Authentication": { + "Jwt": { + "Authority": "http://localhost:80", + "Audience": "weather-api", + "Issuer": "http://localhost:80" + } + }, + "Keycloak": { + "ClientSecret": "clientSecret" } } diff --git a/keycloak/keycloak/Dockerfile b/keycloak/keycloak/Dockerfile index d2312bd..abefe05 100644 --- a/keycloak/keycloak/Dockerfile +++ b/keycloak/keycloak/Dockerfile @@ -1,7 +1,6 @@ FROM quay.io/keycloak/keycloak:latest COPY keycloak.conf /opt/keycloak/conf/keycloak.conf -COPY realm-config.json /opt/keycloak/data/import/ ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] -CMD ["start-dev", "--import-realm"] +CMD ["start-dev"] diff --git a/keycloak/keycloak/realm-config.json b/keycloak/keycloak/realm-config.json deleted file mode 100644 index 04e7c59..0000000 --- a/keycloak/keycloak/realm-config.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "realm": "test-realm", - "enabled": true, - "accessTokenLifespan": 30, - "ssoSessionIdleTimeout": 300, - "ssoSessionMaxLifespan": 600, - "roles": { - "realm": [ - { - "name": "admin", - "description": "Administrator role" - }, - { - "name": "user", - "description": "Standard user role" - } - ] - }, - "clients": [ - { - "clientId": "weather-api", - "redirectUris": ["http://localhost/*"], - "enabled": true, - "publicClient": true, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "standardFlowEnabled": true, - "attributes": { - "access.token.lifespan": "30" - }, - "protocolMappers": [ - { - "name": "audience", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-mapper", - "config": { - "included.client.audience": "weather-api", - "id.token.claim": "false", - "access.token.claim": "true" - } - }, - { - "name": "realm-roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "config": { - "claim.name": "roles", - "jsonType.label": "String", - "multivalued": "true", - "id.token.claim": "true", - "access.token.claim": "true" - } - } - ] - } - ], - "users": [ - { - "username": "admin", - "enabled": true, - "emailVerified": true, - "firstName": "Admin", - "lastName": "User", - "email": "admin@example.com", - "credentials": [ - { - "type": "password", - "value": "admin123", - "temporary": false - } - ], - "realmRoles": ["admin", "user"] - }, - { - "username": "testuser", - "enabled": true, - "emailVerified": true, - "firstName": "Test", - "lastName": "User", - "email": "testuser@example.com", - "credentials": [ - { - "type": "password", - "value": "testpass", - "temporary": false - } - ], - "realmRoles": ["user"] - } - ] -} diff --git a/keycloak/test-api.http b/keycloak/test-api.http deleted file mode 100644 index c0e8b21..0000000 --- a/keycloak/test-api.http +++ /dev/null @@ -1,89 +0,0 @@ -@keycloakUrl = http://localhost:8080 -@weatherServiceUrl = http://localhost:5000 -@realm = test-realm -@username = testuser -@password = testpass - -### - -# @name adminToken -POST {{keycloakUrl}}/realms/master/protocol/openid-connect/token -Content-Type: application/x-www-form-urlencoded - -grant_type=password&client_id=admin-cli&username=admin&password=admin - -### - -# Add User -POST {{keycloakUrl}}/admin/realms/{{realm}}/users -Authorization: Bearer {{adminToken.response.body.access_token}} -Content-Type: application/json - -{ - "username": "{{username}}", - "enabled": true, - "emailVerified": true, - "firstName": "Test", - "lastName": "User", - "email": "testuser@example.com", - "credentials": [ - { - "type": "password", - "value": "{{password}}", - "temporary": false - } - ], - "realmRoles": ["user"], - "attributes": { - "department": ["engineering"] - } -} - -### - -# User List -GET {{keycloakUrl}}/admin/realms/{{realm}}/users -Authorization: Bearer {{adminToken.response.body.access_token}} - -### - -# Find User -GET {{keycloakUrl}}/admin/realms/{{realm}}/users?username={{username}} -Authorization: Bearer {{adminToken.response.body.access_token}} - -### - -# @name login -POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/token -Content-Type: application/x-www-form-urlencoded - -grant_type=password&client_id=weather-api&username={{username}}&password={{password}} - -### - -# Logout -POST {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/logout -Content-Type: application/x-www-form-urlencoded - -client_id=weather-api&refresh_token={{login.response.body.refresh_token}} - -### - -# Without Token -GET {{weatherServiceUrl}}/weather - -### - -# With Token -GET {{weatherServiceUrl}}/weather -Authorization: Bearer {{login.response.body.access_token}} - -### - -# OpenID Configuration -GET {{keycloakUrl}}/realms/{{realm}}/.well-known/openid-configuration - -### - -# Public Keys for JWT -GET {{keycloakUrl}}/realms/{{realm}}/protocol/openid-connect/certs From 6281777480d7e1bb05135dda1f5bf283f9907b94 Mon Sep 17 00:00:00 2001 From: Sefer Mirza Date: Fri, 26 Dec 2025 12:57:54 +0300 Subject: [PATCH 16/16] fix token validation --- keycloak/Ui/index.html | 11 +++-- keycloak/WeatherService/JwtTokenBuilder.cs | 18 ++++---- keycloak/WeatherService/KeycloakService.cs | 10 ++--- keycloak/WeatherService/LoginRequest.cs | 3 ++ keycloak/WeatherService/Program.cs | 48 ++++++++++++++++++---- keycloak/WeatherService/appsettings.json | 1 + 6 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 keycloak/WeatherService/LoginRequest.cs diff --git a/keycloak/Ui/index.html b/keycloak/Ui/index.html index 9c28179..da4d1c9 100644 --- a/keycloak/Ui/index.html +++ b/keycloak/Ui/index.html @@ -36,7 +36,7 @@

Weather App

const res = await fetch(`${backendUrl}/login-by-code`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: `"${code}"` + body: JSON.stringify({ code, redirectUri }) }); if (!res.ok) { @@ -45,7 +45,7 @@

Weather App

const data = await res.json(); - localStorage.setItem('access_token', data.access_token); + localStorage.setItem('accessToken', data.accessToken); await loadWeather(); } catch (err) { @@ -55,9 +55,8 @@

Weather App

} async function loadWeather() { - const token = localStorage.getItem('access_token'); - - if (!token) { + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) { redirectToLogin(); return; } @@ -65,7 +64,7 @@

Weather App

try { const res = await fetch(`${backendUrl}/weather`, { headers: { - Authorization: 'Bearer ' + token + Authorization: 'Bearer ' + accessToken } }); diff --git a/keycloak/WeatherService/JwtTokenBuilder.cs b/keycloak/WeatherService/JwtTokenBuilder.cs index 86c6634..3e9e534 100644 --- a/keycloak/WeatherService/JwtTokenBuilder.cs +++ b/keycloak/WeatherService/JwtTokenBuilder.cs @@ -9,7 +9,7 @@ public class JwtTokenBuilder(IConfiguration _configuration, TimeProvider _timePr { readonly int _defaultExpiresInMinutes = 20; - string Key => _configuration.GetValue($"Authentication:Jwt:{nameof(Key)}", "WeatherService"); + string Key => _configuration.GetValue($"Authentication:Jwt:{nameof(Key)}", "7F9aP2LkQxM4WJtE8RZsD0HnYcB5U3Vv"); string? Issuer => _configuration.GetValue($"Authentication:Jwt:{nameof(Issuer)}"); string? Audience => _configuration.GetValue($"Authentication:Jwt:{nameof(Audience)}"); @@ -18,16 +18,18 @@ public string Build(List claims) var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Key)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - var header = new JwtHeader(creds); - var payload = new JwtPayload( + Console.WriteLine($"Building JWT for Issuer: {Issuer}, Audience: {Audience}"); + + var token = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, - notBefore: null, - issuedAt: _timeProvider.GetUtcNow().DateTime, - expires: _timeProvider.GetUtcNow().AddMinutes(_defaultExpiresInMinutes).DateTime + notBefore: DateTime.UtcNow, + expires: DateTime.UtcNow.AddMinutes(_defaultExpiresInMinutes), + signingCredentials: creds ); - - return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(header, payload)); + var res = new JwtSecurityTokenHandler().WriteToken(token); + Console.WriteLine($"ress {res}"); + return res; } } \ No newline at end of file diff --git a/keycloak/WeatherService/KeycloakService.cs b/keycloak/WeatherService/KeycloakService.cs index 325fe63..7324405 100644 --- a/keycloak/WeatherService/KeycloakService.cs +++ b/keycloak/WeatherService/KeycloakService.cs @@ -5,18 +5,19 @@ namespace WeatherService; public class KeycloakClient(IConfiguration _configuration, HttpClient _httpClient) { private readonly HttpClient _httpClient = _httpClient; - private readonly string _tokenEndpoint = "http://localhost:8080/realms/test-realm/protocol/openid-connect/token"; + private readonly string _tokenEndpoint = "http://keycloak:8080/realms/test-realm/protocol/openid-connect/token"; private readonly string _clientId = "weather-api"; private readonly string _clientSecret = _configuration["Keycloak:ClientSecret"] ?? "clientSecret"; - public async Task GetTokenByCodeAsync(string code) + public async Task GetTokenByCodeAsync(string code, string redirectUri) { var parameters = new Dictionary { { "grant_type", "authorization_code" }, { "code", code }, { "client_id", _clientId }, - { "client_secret", _clientSecret } + { "client_secret", _clientSecret }, + { "redirect_uri", redirectUri } }; var content = new FormUrlEncodedContent(parameters); @@ -28,5 +29,4 @@ public class KeycloakClient(IConfiguration _configuration, HttpClient _httpClien return accessToken; } -} - +} \ No newline at end of file diff --git a/keycloak/WeatherService/LoginRequest.cs b/keycloak/WeatherService/LoginRequest.cs new file mode 100644 index 0000000..60b2d2a --- /dev/null +++ b/keycloak/WeatherService/LoginRequest.cs @@ -0,0 +1,3 @@ +namespace WeatherService; + +public record LoginRequestBody(string Code, string RedirectUri); diff --git a/keycloak/WeatherService/Program.cs b/keycloak/WeatherService/Program.cs index d3ac9ec..67e8228 100644 --- a/keycloak/WeatherService/Program.cs +++ b/keycloak/WeatherService/Program.cs @@ -1,6 +1,8 @@ -using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.Security.Claims; +using System.Text; using WeatherService; var builder = WebApplication.CreateBuilder(args); @@ -16,8 +18,41 @@ }); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(); + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuers = [ + "http://localhost:80", + "http://localhost", + ], + ValidateAudience = true, + ValidAudience = builder.Configuration["Authentication:Jwt:Audience"], + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes( + builder.Configuration["Authentication:Jwt:Key"]! + ) + ), + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = ctx => + { + Console.WriteLine("Auth failed: " + ctx.Exception); + return Task.CompletedTask; + }, + OnTokenValidated = ctx => + { + Console.WriteLine("Token validated successfully"); + return Task.CompletedTask; + } + }; + }); builder.Services.AddAuthorization(); builder.Services.AddSingleton(); @@ -42,13 +77,12 @@ return Results.Ok(weather); }).RequireAuthorization(); - -app.MapPost("/login-by-code", async ([FromServices] JwtTokenBuilder tokenBuilder, [FromServices] KeycloakClient keycloakClient, [FromBody] string code) => +app.MapPost("/login-by-code", async ([FromServices] JwtTokenBuilder tokenBuilder, [FromServices] KeycloakClient keycloakClient, [FromBody] LoginRequestBody body) => { - var accessToken = await keycloakClient.GetTokenByCodeAsync(code); + var accessToken = await keycloakClient.GetTokenByCodeAsync(body.Code, body.RedirectUri); var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(accessToken); - var username = jwt.Claims.FirstOrDefault(c => c.Type == "username")?.Value; + var username = jwt.Claims.FirstOrDefault(c => c.Type == "username" || c.Type == "preferred_username")?.Value; List claims = []; if (!string.IsNullOrEmpty(username)) @@ -57,7 +91,7 @@ } string token = tokenBuilder.Build(claims); - return Results.Ok(new LoginResponse(token)); + return Results.Ok(new LoginResponse(AccessToken: token)); }).AllowAnonymous(); app.Run(); \ No newline at end of file diff --git a/keycloak/WeatherService/appsettings.json b/keycloak/WeatherService/appsettings.json index 0c23753..d129936 100644 --- a/keycloak/WeatherService/appsettings.json +++ b/keycloak/WeatherService/appsettings.json @@ -10,6 +10,7 @@ "Jwt": { "Authority": "http://localhost:80", "Audience": "weather-api", + "Key": "7F9aP2LkQxM4WJtE8RZsD0HnYcB5U3Vv", "Issuer": "http://localhost:80" } },