diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..892890e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,275 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Powershell files +[*.ps1] +indent_size = 2 + +# Shell script files +[*.sh] +end_of_line = lf +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = warning + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:refactoring +dotnet_style_qualification_for_property = false:refactoring +dotnet_style_qualification_for_method = false:refactoring +dotnet_style_qualification_for_event = false:refactoring + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = s_ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' +dotnet_diagnostic.RS2008.severity = none + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = warning + +# RS0016: Only enable if API files are present +dotnet_public_api_analyzer.require_api_files = true + +# CSharp code style settings: +[*.cs] +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +[src/CodeStyle/**.{cs,vb}] +# warning RS0005: Do not use generic CodeAction.Create to create CodeAction +dotnet_diagnostic.RS0005.severity = none + +[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures,VisualStudio}/**/*.{cs,vb}] + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.IDE0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? +# IDE0051: Remove unused private member +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = warning + +# Prefer "var" everywhere +dotnet_diagnostic.IDE0007.severity = warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning + +[src/{VisualStudio}/**/*.{cs,vb}] +# CA1822: Make member static +# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858 +# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT. +dotnet_diagnostic.CA1822.severity = suggestion \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 1ff0c42..ef82812 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto +*.sh text eol=lf ############################################################################### # Set default behavior for command prompt diff. diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..9bf8575 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,53 @@ +name: Docker Image CI + +on: + push: + branches: + - master + tags: + - '*' + + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + + - + name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - + name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: | + ghcr.io/neothor/steam-openid-connect-provider + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: src/ + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 4bda13b..ca3816f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ + +## For secrets in through docker env variables +docker-compose.secrets.yml + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -330,3 +334,5 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ + +dist/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 11e1348..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build -WORKDIR /src -COPY ["src/SteamOpenIdConnectProvider.csproj", "SteamOpenIdConnectProvider/"] -RUN dotnet restore "SteamOpenIdConnectProvider/SteamOpenIdConnectProvider.csproj" -COPY ["src/", "SteamOpenIdConnectProvider/"] -WORKDIR "/src/SteamOpenIdConnectProvider" -RUN dotnet build "SteamOpenIdConnectProvider.csproj" -c Release -o /app - -FROM build AS publish -RUN dotnet publish "SteamOpenIdConnectProvider.csproj" -c Release -o /app - -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base -WORKDIR /app -COPY --from=publish /app . -EXPOSE 80 -HEALTHCHECK CMD curl --fail http://localhost/health || exit 1 -ENTRYPOINT ["dotnet", "SteamOpenIdConnectProvider.dll"] diff --git a/dev.local.plantuml b/dev.local.plantuml new file mode 100644 index 0000000..131a2a5 --- /dev/null +++ b/dev.local.plantuml @@ -0,0 +1,29 @@ +@startuml + +node Host { + component Browser + + node Docker { + component Proxy + component Keycloak + component SteamIdp + component Postgresql + + Proxy --> Keycloak: keycloak (http) + Proxy --> SteamIdp: steamidp (http) + Keycloak --> Postgresql: postgres + + Proxy <-- Keycloak: dev.local (https) + } + + component SteamIdpDev as "SteamIdp" +} + +cloud Internet { + component Steam +} + +Browser --> Proxy: dev.local (https) +Proxy ..> SteamIdpDev: host.docker.internal (http) + +@enduml \ No newline at end of file diff --git a/develop/keycloak/Dockerfile b/develop/keycloak/Dockerfile new file mode 100644 index 0000000..cbc4d2a --- /dev/null +++ b/develop/keycloak/Dockerfile @@ -0,0 +1,19 @@ +FROM jboss/keycloak + +USER root + +# Install sudo and utils to configure jboss user +RUN microdnf update -y && \ + microdnf install -y sudo shadow-utils passwd && \ + microdnf clean all + +# 'Fix' jboss user, add to sudoers +RUN usermod --password jboss jboss && \ + usermod -aG wheel jboss && \ + sed -i 's/# includedir/includedir/' /etc/sudoers && \ + echo "jboss ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/jboss + +ADD ./add-to-truststore.sh /opt/jboss/startup-scripts/add-to-truststore.sh +RUN chmod +x /opt/jboss/startup-scripts/add-to-truststore.sh + +USER jboss \ No newline at end of file diff --git a/develop/keycloak/add-to-truststore.sh b/develop/keycloak/add-to-truststore.sh new file mode 100644 index 0000000..5499c3f --- /dev/null +++ b/develop/keycloak/add-to-truststore.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +CACERTS=$(readlink -e $(dirname $(readlink -e $(which keytool)))"/../lib/security/cacerts") + +sudo keytool \ + -import -trustcacerts \ + -alias "dev.local.crt" -file /tmp/dev.local.crt \ + -keystore ${CACERTS} \ + -storepass changeit \ + -noprompt \ No newline at end of file diff --git a/develop/keycloak/dev-realm.json b/develop/keycloak/dev-realm.json new file mode 100644 index 0000000..91eb7e2 --- /dev/null +++ b/develop/keycloak/dev-realm.json @@ -0,0 +1,1787 @@ +{ + "id": "dev", + "realm": "dev", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRole": { + "id": "e213aa88-c64e-4688-be18-6befd10a896f", + "name": "default-roles-dev", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "dev" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [ + { + "name": "builtin-default-policy", + "builtin": true, + "enable": false + } + ] + }, + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "015f65aa-1d2b-484c-9846-2c1e24cdf32e", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/dev/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/dev/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "43f7defb-7fa1-4c39-88d7-438a9b152563", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/dev/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/dev/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "3e8967f9-8e91-4c5d-8b06-ef9b424dfb45", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "31cdbd7e-d641-4650-973e-75ae93ad9b0e", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "4f2f4ade-2496-470c-bb56-a584da1efa07", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "b1c685e0-b330-425c-a4d8-7b528b7957fb", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "032b6141-9340-4795-b47e-6ce9b0377bdb", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/dev/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/dev/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "c8805da0-194b-4361-b961-d7934849f48c", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "0436adc3-a1ae-408d-b200-030e00c5bc07", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "3f20c76c-6215-4344-888e-b7d61b69af70", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "44cbf50c-ca6f-46a6-b95a-8c29e420e9fe", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "d1f0a11c-8bd2-4d8d-8c1b-4ba40bca9d51", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "727d0335-3f41-412b-a62e-5858a78b58d0", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "265a8134-bce4-41f8-ab50-e1b08ec12ecf", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "1798cf58-6302-4f50-bb43-db0754d3a7c8", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "84535028-529d-48a5-b517-38c3d2083776", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "805aa9fb-d305-4a58-b114-ab1a51518f18", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "47b4b43f-aa19-4352-9e84-f02b64377b27", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "a9f17bbe-56cc-4208-a971-b0bce597ec67", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "b302846b-f739-4566-8092-32436b1fd3b5", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "e9286ba1-25ba-4ed6-bf0d-3382c7e54133", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "e88f9947-2012-4171-ba79-2a7cfb071e72", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "22d76b1d-528b-4eee-8873-5e0cd8a9f156", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "04c470b8-0ab4-4f70-8fe0-2f02fe4d6dfb", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "1bd9c2fb-3a76-49f8-9511-b71aa728c9a5", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "6b479a64-0127-46be-9e34-46eaad12a49b", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "64a1555f-ac9f-4fad-aa20-b63976b7c8e8", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "d48dfbcb-cfb6-436b-84cd-e2d03363a6fc", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "f6c3e4fc-2039-4443-819d-abd4ec458e98", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "3b8b9e0c-3afc-415a-b550-91780ec44af1", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "198ce177-b1b6-4364-9380-cc4d4f64615b", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "b6c011b9-e953-4a2e-90f1-fd96a2ab6827", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "12c25a87-7670-4f59-913c-39c69ef9f306", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "47e27457-e550-4df8-b49d-18e0e898f836", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "fafef84a-3bb8-49e7-83f2-b64208cf43ac", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "57c538c1-2544-4302-b040-38722c73335e", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "a0bf0b01-a914-4fd6-a2bf-af5b12ddf231", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "2cbb508b-e085-4ff3-a4d5-394fcd16a6ef", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "aaf8fa54-2467-4b16-888c-5f41b0cc87b5", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "37ea9623-505e-4e84-afd4-d2397f986f4f", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "57f57cc2-ac22-4ecf-aa2f-05c5fbee7061", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "e8f29966-d46a-43e6-a845-69402f621183", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "dd38103c-ed2b-4845-a12c-42d261fb90e4", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [ + { + "alias": "steam", + "internalId": "4371eef8-d3d8-42fa-aa7a-1b08052bb007", + "providerId": "oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": false, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "validateSignature": "true", + "userInfoUrl": "https://dev.local/steam/connect/userinfo", + "tokenUrl": "https://dev.local/steam/connect/token", + "clientId": "keycloak", + "jwksUrl": "https://dev.local/steam/.well-known/openid-configuration/jwks", + "issuer": "https://dev.local/steam", + "useJwksUrl": "true", + "authorizationUrl": "https://dev.local/steam/connect/authorize", + "clientAuthMethod": "client_secret_basic", + "logoutUrl": "https://dev.local/steam/connect/endsession", + "syncMode": "FORCE", + "clientSecret": "**********", + "allowedClockSkew": "10", + "defaultScope": "openid profile" + } + } + ], + "identityProviderMappers": [ + { + "id": "5bb308fe-c9a9-4565-8851-614628cac5b1", + "name": "picture", + "identityProviderAlias": "steam", + "identityProviderMapper": "oidc-user-attribute-idp-mapper", + "config": { + "syncMode": "INHERIT", + "claim": "picture", + "user.attribute": "picture" + } + }, + { + "id": "cfee9d15-0c9b-49b1-9d6a-a3118715be97", + "name": "website", + "identityProviderAlias": "steam", + "identityProviderMapper": "oidc-user-attribute-idp-mapper", + "config": { + "syncMode": "INHERIT", + "claim": "website", + "user.attribute": "website" + } + } + ], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "cf4793d9-960b-4c1a-82cd-1456b09ffebe", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "0ea56136-cc82-42c0-9cfc-751e63aaecee", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "cbd298fe-6251-47d5-83df-7346912a5aad", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "ff5c15ec-f38c-405f-aefe-1bae8a0352e1", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "e45c806d-6794-4507-acb0-316b45f17901", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-full-name-mapper", + "oidc-address-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "a8deab80-df62-4e17-913b-6767624275bd", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "3b0c05c2-5f1e-4eac-aa00-14d7c8e9c488", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "9cc2c69f-40ef-4013-a777-6b58ae85ffa6", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "510f8359-39e1-46f4-a844-b868c8b8da93", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "ac2f383b-eb4d-460f-8b0a-f6c5d9cba979", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "d27999f5-3645-401e-ad3c-d5f57dd6704c", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "332c7de2-7ba1-4cc0-be24-559f32ee51e4", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "c9f45b1f-4ab1-42f4-8206-b354d0a42f9d", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "3a37ad54-e016-497f-8dde-9fd91d811ebe", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "69c205d1-8648-46fb-a524-38faf5de8355", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "7f0300d2-c749-4fab-8a40-a0f436e9b941", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "c00cca79-b557-4acf-87e6-f888537e8003", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "5a97e90c-c3a7-45b3-8ce1-7c4b1f823cc3", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "cdb342f7-93fc-4d04-b0bc-e4a7c6f59cc6", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "a8dcd3bf-72f6-4442-9f37-64d5f5680bde", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "973ef407-d084-4675-968f-3244a4f214f7", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "50ff1818-d89a-419d-a1fc-f8f8d2549572", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9cfca69a-1d2f-4dc6-a989-4131e8fdc20f", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "fbef5afc-bd23-4810-ad18-1b5f07349b49", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "e47ec7d5-3a66-4d85-9f18-d882b3ff12b4", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "da238f3d-11c7-48bf-8fb5-49313b932c9b", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "1e088135-b96f-4f4f-a4ec-557c74b1850e", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "8e7ea565-adf6-46ac-811e-ba4aa9cc7e96", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "cb590839-546a-4bab-953d-347b1416af36", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "df52563c-689f-4148-82a0-18d0b30af265", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "2a7a7244-047b-447b-a6e6-0a9812ab95cc", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "747e1ea6-8438-43a7-9c21-ca5b3448787b", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "74ab3ebf-67d4-4337-89d7-a17c9bbae187", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "cibaInterval": "5" + }, + "keycloakVersion": "13.0.0", + "userManagedAccessAllowed": false +} \ No newline at end of file diff --git a/develop/proxy/Dockerfile b/develop/proxy/Dockerfile new file mode 100644 index 0000000..38c3a8e --- /dev/null +++ b/develop/proxy/Dockerfile @@ -0,0 +1,9 @@ +FROM nginx + +ENV SSL_CERT=/tmp/dev.local.crt +ENV SSL_KEY=/tmp/dev.local.key + +ENV KEYCLOAK_URI=http://keycloak:8080 +ENV STEAMIDP_URI=http://steamidp:80 + +ADD ./proxy_ssl.conf.template /etc/nginx/templates/ \ No newline at end of file diff --git a/develop/proxy/proxy_ssl.conf.template b/develop/proxy/proxy_ssl.conf.template new file mode 100644 index 0000000..00ff768 --- /dev/null +++ b/develop/proxy/proxy_ssl.conf.template @@ -0,0 +1,21 @@ +server { + listen 443 ssl; + ssl_certificate ${SSL_CERT}; + ssl_certificate_key ${SSL_KEY}; + + proxy_set_header X-Forwarded-For $proxy_protocol_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + + location / { + return 301 https://$host/auth/realms/dev/account; + } + + location /auth { + proxy_pass ${KEYCLOAK_URI}; + } + + location /steam { + proxy_pass ${STEAMIDP_URI}; + } +} \ No newline at end of file diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml new file mode 100644 index 0000000..85b2a37 --- /dev/null +++ b/docker-compose.debug.yml @@ -0,0 +1,7 @@ +version: '2' +services: + proxy: + environment: + STEAMIDP_URI: https://steamidp-debugger:5001 + extra_hosts: + - "steamidp-debugger:host-gateway" \ No newline at end of file diff --git a/docker-compose.secrets.yml.template b/docker-compose.secrets.yml.template new file mode 100644 index 0000000..c32387a --- /dev/null +++ b/docker-compose.secrets.yml.template @@ -0,0 +1,14 @@ +version: '2' +services: + proxy: + volumes: + - "/dev.local.crt:/tmp/dev.local.crt" + - "/dev.local.key:/tmp/dev.local.key" + + keycloak: + volumes: + - "/dev.local.crt:/tmp/dev.local.crt" + + steamidp: + environment: + Steam__ApplicationKey: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9a2f38d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: '2' +volumes: + postgres_data: + driver: local + +services: + proxy: + image: neothor/proxy:develop + build: ./develop/proxy + container_name: proxy + ports: + - 443:443 + links: + - keycloak + - steamidp + + postgres: + image: postgres + container_name: postgres + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + + keycloak: + image: neothor/keycloak:develop + build: ./develop/keycloak + container_name: keycloak + environment: + DB_VENDOR: POSTGRES + DB_ADDR: postgres + DB_DATABASE: keycloak + DB_USER: keycloak + DB_SCHEMA: public + DB_PASSWORD: password + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: changeit + PROXY_ADDRESS_FORWARDING: "true" + links: + - postgres + extra_hosts: + - "dev.local:host-gateway" + + steamidp: + image: neothor/steam-openid-connect-provider:develop + build: ./src + container_name: steamidp + links: + - keycloak + environment: + OpenID__ClientID: keycloak + OpenID__ClientName: keycloak + OpenId__ClientSecret: keycloak + OpenID__RedirectUri: https://dev.local/auth/realms/dev/broker/steam/endpoint + Hosting__BasePath: /steam \ No newline at end of file diff --git a/src/Controllers/ExternalLoginController.cs b/src/Controllers/ExternalLoginController.cs index 67b54ec..5aa49df 100644 --- a/src/Controllers/ExternalLoginController.cs +++ b/src/Controllers/ExternalLoginController.cs @@ -1,33 +1,48 @@ using System; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using IdentityServer4.Events; +using IdentityServer4.Extensions; +using IdentityServer4.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SteamOpenIdConnectProvider.Domains.IdentityServer; namespace SteamOpenIdConnectProvider.Controllers { [AllowAnonymous] - [Route("[action]")] public class ExternalLoginController : Controller { private readonly SignInManager _signInManager; private readonly UserManager _userManager; + private readonly IIdentityServerInteractionService _interaction; + private readonly IEventService _events; + + private readonly OpenIdConfig _config; private readonly ILogger _logger; public ExternalLoginController( SignInManager signInManager, UserManager userManager, + IIdentityServerInteractionService interaction, + IEventService events, + IOptions config, ILogger logger) { _signInManager = signInManager; _userManager = userManager; + _config = config.Value; _logger = logger; + _interaction = interaction; + _events = events; } - [HttpGet] + [HttpGet("external-login")] public Task ExternalLogin(string returnUrl = null) { const string provider = "Steam"; @@ -37,11 +52,11 @@ namespace SteamOpenIdConnectProvider.Controllers return Task.FromResult(new ChallengeResult(provider, properties)); } - [HttpGet] + [HttpGet("external-login-callback")] public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) { returnUrl ??= Url.Content("~/"); - + if (remoteError != null) { throw new Exception($"Error from external provider: {remoteError}"); @@ -62,7 +77,17 @@ namespace SteamOpenIdConnectProvider.Controllers var userName = info.Principal.FindFirstValue(ClaimTypes.Name); var userId = info.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - + + if (string.IsNullOrEmpty(userId)) + { + throw new ArgumentNullException(nameof(userId), $"No claim found for {ClaimTypes.NameIdentifier}"); + } + + if (string.IsNullOrEmpty(userName)) + { + userName = userId.Split('/').Last(); + } + var user = new IdentityUser { UserName = userName, Id = userId }; _userManager.UserValidators.Clear(); @@ -86,5 +111,21 @@ namespace SteamOpenIdConnectProvider.Controllers return BadRequest(); } + + [HttpGet("external-logout")] + public async Task ExternalLogout(string logoutId) + { + var logout = await _interaction.GetLogoutContextAsync(logoutId); + + if (User?.Identity.IsAuthenticated == true) + { + await _signInManager.SignOutAsync(); + await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); + } + + return Redirect(logout?.PostLogoutRedirectUri ?? + _config.PostLogoutRedirectUris.FirstOrDefault() ?? + Url.Content("~/")); + } } } diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..dce0b07 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,26 @@ +FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build + +WORKDIR /src + +# Copy the project file to create layer with packages +COPY SteamOpenIdConnectProvider.csproj . +RUN dotnet restore ./SteamOpenIdConnectProvider.csproj + +# Copy the rest of the source +COPY . . +RUN dotnet build ./SteamOpenIdConnectProvider.csproj -c Release -o /app + +FROM build AS publish +RUN dotnet publish ./SteamOpenIdConnectProvider.csproj -c Release -o /app + +FROM mcr.microsoft.com/dotnet/aspnet:5.0 + +RUN apt-get update && \ + apt-get install -y curl + +WORKDIR /app +COPY --from=publish /app . +EXPOSE 80 + +HEALTHCHECK CMD curl --fail http://localhost/health || exit 1 +ENTRYPOINT ["dotnet", "SteamOpenIdConnectProvider.dll"] \ No newline at end of file diff --git a/src/Domains/Common/HostingConfig.cs b/src/Domains/Common/HostingConfig.cs new file mode 100644 index 0000000..d63f374 --- /dev/null +++ b/src/Domains/Common/HostingConfig.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SteamOpenIdConnectProvider.Domains.Common +{ + public class HostingConfig + { + public static readonly string Key = "Hosting"; + + public string BasePath { get; set; } + + public string PublicOrigin { get; set; } + } +} diff --git a/src/Database/AppInMemoryDbContext.cs b/src/Domains/IdentityServer/AppInMemoryDbContext.cs similarity index 87% rename from src/Database/AppInMemoryDbContext.cs rename to src/Domains/IdentityServer/AppInMemoryDbContext.cs index 0284829..7450f17 100644 --- a/src/Database/AppInMemoryDbContext.cs +++ b/src/Domains/IdentityServer/AppInMemoryDbContext.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace SteamOpenIdConnectProvider.Database +namespace SteamOpenIdConnectProvider.Domains.IdentityServer { // This is completely in-memory, we do not need a persistent store. public class AppInMemoryDbContext : IdentityDbContext diff --git a/src/IdentityServerConfig.cs b/src/Domains/IdentityServer/IdentityServerConfigFactory.cs similarity index 61% rename from src/IdentityServerConfig.cs rename to src/Domains/IdentityServer/IdentityServerConfigFactory.cs index da32c4b..ba41e9f 100644 --- a/src/IdentityServerConfig.cs +++ b/src/Domains/IdentityServer/IdentityServerConfigFactory.cs @@ -1,33 +1,35 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; +using System.Linq; using IdentityServer4; using IdentityServer4.Models; +using SteamOpenIdConnectProvider.Domains.IdentityServer; -namespace SteamOpenIdConnectProvider +namespace SteamOpenIdConnectProvider.Models.IdentityServer { - public class IdentityServerConfig + public static class IdentityServerConfigFactory { - public static IEnumerable GetClients(string clientId, string secret, string redirectUri, string logoutRedirectUri) + public static IEnumerable GetClients(OpenIdConfig config) { - yield return new Client + var client = new Client { - ClientId = clientId, - ClientName = "Proxy Client", + ClientId = config.ClientID, + ClientName = config.ClientName, AllowedGrantTypes = GrantTypes.Code, RequireConsent = false, ClientSecrets = { - new Secret(secret.Sha256()) + new Secret(config.ClientSecret.Sha256()) }, AlwaysSendClientClaims = true, AlwaysIncludeUserClaimsInIdToken = true, // where to redirect to after login - RedirectUris = redirectUri.Split(",").Select(x => x.Trim()).ToArray(), + RedirectUris = config.RedirectUris.ToArray(), // where to redirect to after logout - PostLogoutRedirectUris = { logoutRedirectUri }, + PostLogoutRedirectUris = config.PostLogoutRedirectUris.ToArray(), + RequirePkce = false, AllowedScopes = new List { @@ -35,6 +37,7 @@ namespace SteamOpenIdConnectProvider IdentityServerConstants.StandardScopes.Profile, } }; + yield return client; } public static IEnumerable GetIdentityResources() @@ -46,4 +49,4 @@ namespace SteamOpenIdConnectProvider }; } } -} \ No newline at end of file +} diff --git a/src/Domains/IdentityServer/OpenIdConfig.cs b/src/Domains/IdentityServer/OpenIdConfig.cs new file mode 100644 index 0000000..f858529 --- /dev/null +++ b/src/Domains/IdentityServer/OpenIdConfig.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SteamOpenIdConnectProvider.Domains.IdentityServer +{ + public class OpenIdConfig + { + public static readonly string Key = "OpenID"; + + public string ClientID { get; set; } + public string ClientSecret { get; set; } + public string RedirectUri { get; set; } + public string PostLogoutRedirectUri { get; set; } + public string ClientName { get; set; } = "Proxy Client"; + + public IEnumerable RedirectUris => (RedirectUri ?? string.Empty).Split( + new[] { ',', ';' }, + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + public IEnumerable PostLogoutRedirectUris => (PostLogoutRedirectUri ?? string.Empty).Split( + new[] { ',', ';' }, + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + } +} diff --git a/src/Domains/IdentityServer/OpenIdStandardClaims.cs b/src/Domains/IdentityServer/OpenIdStandardClaims.cs new file mode 100644 index 0000000..f415a5b --- /dev/null +++ b/src/Domains/IdentityServer/OpenIdStandardClaims.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SteamOpenIdConnectProvider.Domains.IdentityServer +{ + public static class OpenIdStandardClaims + { + public static readonly string Name = "name"; + public static readonly string GivenName = "given_name"; + public static readonly string FamilyName = "family_name"; + public static readonly string MiddleName = "middle_name"; + public static readonly string Nickname = "nickname"; + public static readonly string PreferredUsername = "preferred_username"; + public static readonly string Profile = "profile"; + public static readonly string Picture = "picture"; + public static readonly string Website = "website"; + public static readonly string Email = "email"; + public static readonly string EmailVerified = "email_verified"; + public static readonly string Gender = "gender"; + public static readonly string BirthDate = "BirthDate"; + public static readonly string Zoneinfo = "zoneinfo"; + public static readonly string Locale = "locale"; + public static readonly string PhoneNumber = "phone_number"; + public static readonly string PhoneNumberVerified = "phone_number_verified"; + public static readonly string Address = "address"; + public static readonly string UpdatedAt = "updated_at"; + } +} diff --git a/src/Domains/IdentityServer/SteamClaims.cs b/src/Domains/IdentityServer/SteamClaims.cs new file mode 100644 index 0000000..e557d46 --- /dev/null +++ b/src/Domains/IdentityServer/SteamClaims.cs @@ -0,0 +1,7 @@ +namespace SteamOpenIdConnectProvider.Domains.IdentityServer +{ + public static class SteamClaims + { + public static readonly string SteamId = "steam_id"; + } +} diff --git a/src/Domains/Steam/Constants.cs b/src/Domains/Steam/Constants.cs new file mode 100644 index 0000000..4863d1b --- /dev/null +++ b/src/Domains/Steam/Constants.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SteamOpenIdConnectProvider.Domains.Steam +{ + public static class Constants + { + public static readonly string OpenIdUrl = "https://steamcommunity.com/openid/id/"; + public static readonly string ApiUrl = "https://api.steampowered.com/"; + public static readonly string GetPlayerSummariesUrl = ApiUrl + "ISteamUser/GetPlayerSummaries/v0002"; + } +} diff --git a/src/Domains/Steam/GetPlayerSummariesResponse.cs b/src/Domains/Steam/GetPlayerSummariesResponse.cs new file mode 100644 index 0000000..0cf516a --- /dev/null +++ b/src/Domains/Steam/GetPlayerSummariesResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using SteamOpenIdConnectProvider.Profile.Models; + +namespace SteamOpenIdConnectProvider.Domains.Steam +{ + public sealed class GetPlayerSummariesResponse + { + [JsonPropertyName("players")] + public ICollection Players { get; set; } + } +} \ No newline at end of file diff --git a/src/Domains/Steam/Player.cs b/src/Domains/Steam/Player.cs new file mode 100644 index 0000000..6ce9ae6 --- /dev/null +++ b/src/Domains/Steam/Player.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SteamOpenIdConnectProvider.Profile.Models +{ + public sealed class Player + { + [JsonPropertyName("steamid")] + public string SteamId { get; set; } + + [JsonPropertyName("communityvisibilitystate")] + public int CommunityVisibilityState { get; set; } + + [JsonPropertyName("profilestate")] + public int ProfileState { get; set; } + + [JsonPropertyName("personaname")] + public string PersonaName { get; set; } + + [JsonPropertyName("commentpermission")] + public int CommentPermission { get; set; } + + [JsonPropertyName("profileurl")] + public string ProfileUrl { get; set; } + + [JsonPropertyName("avatar")] + public string Avatar { get; set; } + + [JsonPropertyName("avatarmedium")] + public string AvatarMedium { get; set; } + + [JsonPropertyName("avatarfull")] + public string AvatarFull { get; set; } + + [JsonPropertyName("avatarhash")] + public string AvatarHash { get; set; } + + [JsonPropertyName("lastlogoff")] + public int LastLogoff { get; set; } + + [JsonPropertyName("personastate")] + public int PersonaState { get; set; } + + [JsonPropertyName("realname")] + public string RealName { get; set; } + + [JsonPropertyName("primaryclanid")] + public string PrimaryClanId { get; set; } + + [JsonPropertyName("timecreated")] + public int TimeCreated { get; set; } + + [JsonPropertyName("personastateflags")] + public int PersonaStateFlags { get; set; } + + [JsonPropertyName("loccountrycode")] + public string LocCountryCode { get; set; } + } +} diff --git a/src/Domains/Steam/SteamConfig.cs b/src/Domains/Steam/SteamConfig.cs new file mode 100644 index 0000000..6a7e40e --- /dev/null +++ b/src/Domains/Steam/SteamConfig.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SteamOpenIdConnectProvider.Domains.Steam +{ + public class SteamConfig + { + public static readonly string Key = "Steam"; + + public string ApplicationKey { get; set; } + } +} diff --git a/src/Profile/SteamProfileService.cs b/src/Domains/Steam/SteamProfileService.cs similarity index 55% rename from src/Profile/SteamProfileService.cs rename to src/Domains/Steam/SteamProfileService.cs index 795f8b9..a2f9465 100644 --- a/src/Profile/SteamProfileService.cs +++ b/src/Domains/Steam/SteamProfileService.cs @@ -1,46 +1,40 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Security.Claims; +using System.Text.Json; using System.Threading.Tasks; using IdentityServer4.Extensions; using IdentityServer4.Models; using IdentityServer4.Services; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using SteamOpenIdConnectProvider.Profile.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SteamOpenIdConnectProvider.Domains.IdentityServer; +using SteamOpenIdConnectProvider.Domains.Steam; +using SteamOpenIdConnectProvider.Models.Steam; -namespace SteamOpenIdConnectProvider.Profile +namespace SteamOpenIdConnectProvider.Services { public class SteamProfileService : IProfileService { private readonly HttpClient _httpClient; - private readonly IConfiguration _configuration; + private readonly SteamConfig _config; private readonly IUserClaimsPrincipalFactory _claimsFactory; + private readonly ILogger _logger; private readonly UserManager _userManager; - private async Task GetPlayerSummariesAsync(IEnumerable steamIds) - { - const string baseurl = "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002"; - - var applicationKey = _configuration["Authentication:Steam:ApplicationKey"]; - var url = $"{baseurl}/?key={applicationKey}&steamids={string.Join(',', steamIds)}"; - - var res = await _httpClient.GetStringAsync(url); - var response = JsonConvert.DeserializeObject>(res); - return response.Response; - } - public SteamProfileService( UserManager userManager, IUserClaimsPrincipalFactory claimsFactory, - IConfiguration configuration, + IOptions config, + ILogger logger, HttpClient httpClient) { _userManager = userManager; _claimsFactory = claimsFactory; - _configuration = configuration; + _logger = logger; + _config = config.Value; _httpClient = httpClient; } @@ -53,23 +47,42 @@ namespace SteamOpenIdConnectProvider.Profile var claims = principal.Claims.ToList(); claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList(); - const string steamUrl = "https://steamcommunity.com/openid/id/"; - var steamId = sub.Substring(steamUrl.Length); + var steamId = sub.Substring(Constants.OpenIdUrl.Length); + AddClaim(claims, SteamClaims.SteamId, steamId); var userSummary = await GetPlayerSummariesAsync(new[] { steamId }); var player = userSummary.Players.FirstOrDefault(); if (player != null) { - AddClaim(claims, "picture", player.AvatarFull); - AddClaim(claims, "nickname", player.PersonaName); - AddClaim(claims, "given_name", player.RealName); - AddClaim(claims, "website", player.ProfileUrl); + AddClaim(claims, OpenIdStandardClaims.Picture, player.AvatarFull); + AddClaim(claims, OpenIdStandardClaims.Nickname, player.PersonaName); + AddClaim(claims, OpenIdStandardClaims.PreferredUsername, player.PersonaName); + AddClaim(claims, OpenIdStandardClaims.GivenName, player.RealName); + AddClaim(claims, OpenIdStandardClaims.Website, player.ProfileUrl); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + foreach (var claim in claims) + { + _logger.LogDebug("Issued claim {claim}:{value} for {principle}", + claim.Type, + claim.Value, + principal.Identity.Name); + } } context.IssuedClaims = claims; } + public async Task IsActiveAsync(IsActiveContext context) + { + var sub = context.Subject.GetSubjectId(); + var user = await _userManager.FindByIdAsync(sub); + context.IsActive = user != null; + } + private void AddClaim(List claims, string type, string value) { if (!string.IsNullOrEmpty(value)) @@ -78,11 +91,12 @@ namespace SteamOpenIdConnectProvider.Profile } } - public async Task IsActiveAsync(IsActiveContext context) + private async Task GetPlayerSummariesAsync(IEnumerable steamIds) { - var sub = context.Subject.GetSubjectId(); - var user = await _userManager.FindByIdAsync(sub); - context.IsActive = user != null; + var url = $"{Constants.GetPlayerSummariesUrl}/?key={_config.ApplicationKey}&steamids={string.Join(',', steamIds)}"; + var res = await _httpClient.GetStringAsync(url); + var response = JsonSerializer.Deserialize>(res); + return response.Response; } } -} \ No newline at end of file +} diff --git a/src/Domains/Steam/SteamResponse.cs b/src/Domains/Steam/SteamResponse.cs new file mode 100644 index 0000000..f6647fb --- /dev/null +++ b/src/Domains/Steam/SteamResponse.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace SteamOpenIdConnectProvider.Models.Steam +{ + public sealed class SteamResponse + { + [JsonPropertyName("response")] + public T Response { get; set; } + } +} \ No newline at end of file diff --git a/src/Profile/Models/GetPlayerSummariesResponse.cs b/src/Profile/Models/GetPlayerSummariesResponse.cs deleted file mode 100644 index 12cb571..0000000 --- a/src/Profile/Models/GetPlayerSummariesResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace SteamOpenIdConnectProvider.Profile.Models -{ - public sealed class GetPlayerSummariesResponse - { - [JsonProperty("players")] - public ICollection Players { get; set; } - } -} \ No newline at end of file diff --git a/src/Profile/Models/Player.cs b/src/Profile/Models/Player.cs deleted file mode 100644 index 74af6d6..0000000 --- a/src/Profile/Models/Player.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Newtonsoft.Json; - -namespace SteamOpenIdConnectProvider.Profile.Models -{ - public sealed class Player - { - [JsonProperty("steamid")] - public ulong SteamId { get; set; } - - [JsonProperty("communityvisibilitystate")] - public int CommunityVisibilityState { get; set; } - - [JsonProperty("profilestate")] - public int ProfileState { get; set; } - - [JsonProperty("personaname")] - public string PersonaName { get; set; } - - [JsonProperty("commentpermission")] - public int CommentPermission { get; set; } - - [JsonProperty("profileurl")] - public string ProfileUrl { get; set; } - - [JsonProperty("avatar")] - public string Avatar { get; set; } - - [JsonProperty("avatarmedium")] - public string AvatarMedium { get; set; } - - [JsonProperty("avatarfull")] - public string AvatarFull { get; set; } - - [JsonProperty("avatarhash")] - public string AvatarHash { get; set; } - - [JsonProperty("lastlogoff")] - public int LastLogoff { get; set; } - - [JsonProperty("personastate")] - public int PersonaState { get; set; } - - [JsonProperty("realname")] - public string RealName { get; set; } - - [JsonProperty("primaryclanid")] - public ulong PrimaryClanId { get; set; } - - [JsonProperty("timecreated")] - public int TimeCreated { get; set; } - - [JsonProperty("personastateflags")] - public int PersonaStateFlags { get; set; } - - [JsonProperty("loccountrycode")] - public string LocCountryCode { get; set; } - } -} \ No newline at end of file diff --git a/src/Profile/Models/SteamResponse.cs b/src/Profile/Models/SteamResponse.cs deleted file mode 100644 index 2b43afb..0000000 --- a/src/Profile/Models/SteamResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace SteamOpenIdConnectProvider.Profile.Models -{ - public sealed class SteamResponse - { - [JsonProperty("response")] - public T Response { get; set; } - } -} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index c345b04..c7cbf68 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore; +using System; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; @@ -8,24 +10,42 @@ namespace SteamOpenIdConnectProvider { public class Program { - public static void Main(string[] args) + public static int Main(string[] args) { Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .MinimumLevel.Override("System", LogEventLevel.Warning) - .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) .Enrich.FromLogContext() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate) - .CreateLogger(); + .WriteTo.Console() + .CreateBootstrapLogger(); - CreateWebHostBuilder(args).Build().Run(); + try + { + Log.Information("Starting web host"); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } } - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseKestrel() - .UseSerilog() - .UseStartup(); + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .MinimumLevel.Is(context.HostingEnvironment.IsDevelopment() ? LogEventLevel.Debug : LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console(theme: AnsiConsoleTheme.Code)) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } } diff --git a/src/Startup.cs b/src/Startup.cs index 7a98d35..b8c16cd 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -1,5 +1,4 @@ using System; -using System.Net.Http; using IdentityServer4.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -12,8 +11,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using IdentityServer4.Services; using Microsoft.AspNetCore.HttpOverrides; -using SteamOpenIdConnectProvider.Database; -using SteamOpenIdConnectProvider.Profile; +using SteamOpenIdConnectProvider.Services; +using SteamOpenIdConnectProvider.Models.IdentityServer; +using SteamOpenIdConnectProvider.Domains.Common; +using SteamOpenIdConnectProvider.Domains.IdentityServer; +using SteamOpenIdConnectProvider.Domains.Steam; +using System.IO; +using System.Text; +using Serilog; +using Microsoft.Extensions.Logging; namespace SteamOpenIdConnectProvider { @@ -29,10 +35,8 @@ namespace SteamOpenIdConnectProvider public void ConfigureServices(IServiceCollection services) { services.AddControllers() - .AddNewtonsoftJson() .SetCompatibilityVersion(CompatibilityVersion.Version_3_0); - services.AddSingleton(Configuration); services.AddDbContext(options => options.UseInMemoryDatabase("default")); @@ -43,21 +47,24 @@ namespace SteamOpenIdConnectProvider .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - services.AddIdentityServer(options => + var openIdConfig = Configuration.GetSection(OpenIdConfig.Key); + services + .Configure(openIdConfig) + .AddIdentityServer(options => { - options.UserInteraction.LoginUrl = "/ExternalLogin"; + options.UserInteraction.LoginUrl = "/external-login"; + options.UserInteraction.LogoutUrl = "/external-logout"; }) .AddAspNetIdentity() - .AddInMemoryClients(IdentityServerConfig.GetClients( - Configuration["OpenID:ClientID"], - Configuration["OpenID:ClientSecret"], - Configuration["OpenID:RedirectUri"], - Configuration["OpenID:PostLogoutRedirectUri"])) + .AddInMemoryClients(IdentityServerConfigFactory.GetClients(openIdConfig.Get())) .AddInMemoryPersistedGrants() .AddDeveloperSigningCredential(true) - .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources()); + .AddInMemoryIdentityResources(IdentityServerConfigFactory.GetIdentityResources()); - services.AddHttpClient(); + var steamConfig = Configuration.GetSection(SteamConfig.Key); + services + .Configure(steamConfig) + .AddHttpClient(); services.AddAuthentication() .AddCookie(options => @@ -67,32 +74,37 @@ namespace SteamOpenIdConnectProvider }) .AddSteam(options => { - options.ApplicationKey = Configuration["Authentication:Steam:ApplicationKey"]; + options.ApplicationKey = steamConfig.Get().ApplicationKey; }); services.AddHealthChecks() - .AddUrlGroup(new Uri("https://steamcommunity.com/openid"), "Steam"); + .AddUrlGroup(new Uri(Constants.OpenIdUrl), "Steam"); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + var logger = app.ApplicationServices.GetRequiredService>(); + if (env.IsDevelopment()) { + logger.LogWarning("Starting up in development mode"); app.UseDeveloperExceptionPage(); } - if (!string.IsNullOrEmpty(Configuration["Hosting:PathBase"])) + var hostingConfig = Configuration.GetSection(HostingConfig.Key).Get(); + if (!string.IsNullOrEmpty(hostingConfig.BasePath)) { - app.UsePathBase(Configuration["Hosting:PathBase"]); + app.UsePathBase(hostingConfig.BasePath); } + app.UseSerilogRequestLogging(); + app.UseCookiePolicy(); app.Use(async (ctx, next) => { - var origin = Configuration["Hosting:PublicOrigin"]; - if (!string.IsNullOrEmpty(origin)) + if (!string.IsNullOrEmpty(hostingConfig.PublicOrigin)) { - ctx.SetIdentityServerOrigin(origin); + ctx.SetIdentityServerOrigin(hostingConfig.PublicOrigin); } await next(); diff --git a/src/SteamOpenIdConnectProvider.csproj b/src/SteamOpenIdConnectProvider.csproj index 1b3243f..71c43d3 100644 --- a/src/SteamOpenIdConnectProvider.csproj +++ b/src/SteamOpenIdConnectProvider.csproj @@ -1,29 +1,25 @@  - netcoreapp3.1 + net5.0 InProcess 9f8cb9ce-f696-422f-901a-563af9413684 8 - - - - - - + + + + - - - - - - + + + + - - + + diff --git a/SteamOpenIdConnectProvider.sln b/src/SteamOpenIdConnectProvider.sln similarity index 91% rename from SteamOpenIdConnectProvider.sln rename to src/SteamOpenIdConnectProvider.sln index 0d0559d..b66f6c6 100644 --- a/SteamOpenIdConnectProvider.sln +++ b/src/SteamOpenIdConnectProvider.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28803.202 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamOpenIdConnectProvider", "src\SteamOpenIdConnectProvider.csproj", "{39261E1A-B9AB-45C1-991E-B88ED3E076A2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamOpenIdConnectProvider", "SteamOpenIdConnectProvider.csproj", "{39261E1A-B9AB-45C1-991E-B88ED3E076A2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/appsettings.Development.json b/src/appsettings.Development.json index 9988482..2ada604 100644 --- a/src/appsettings.Development.json +++ b/src/appsettings.Development.json @@ -1,12 +1,5 @@ { - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - }, "Hosting": { - "PathBase": "/test" + "BasePath": "/steam" } } diff --git a/src/appsettings.json b/src/appsettings.json index 5582ca6..f334f2f 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -1,17 +1,18 @@ { - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "OpenID": { - "ClientID": "proxy", - "ClientSecret": "secret", - "RedirectUri": "http://localhost:8080/auth/realms/master/broker/steam/endpoint", - "PostLogoutRedirectUri": "" - }, "AllowedHosts": "*", + "OpenID": { + "ClientID": "keycloak", + "ClientSecret": "keycloak", + "ClientName": "keycloak", + "RedirectUri": "https://dev.local/auth/realms/dev/broker/steam/endpoint", + + // TODO: Don't think this is how it suppose to work. + "PostLogoutRedirectUri": "https://dev.local/auth/realms/dev/protocol/openid-connect/logout?initiating_idp=steam" + }, "Hosting": { - "PathBase": "" + "BasePath": "" + }, + "Steam": { + "ApplicationKey": "secret" } } \ No newline at end of file