Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADMINAPI-1145] Admin API support for Keycloak #230

Merged
merged 7 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
API_MODE=SharedInstance
ODS_VIRTUAL_NAME=api
ADMIN_API_VIRTUAL_NAME=adminapi
LOGS_FOLDER=/tmp/logs

# For Authentication
ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME}
SIGNING_KEY=TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=

PAGING_OFFSET=0
PAGING_LIMIT=25

# The following needs to be set to specify the ODS API endpoint for Admin API to internally connect.
# If user chooses direct connection between ODS API and Admin API within docker network, then set the api internal url as follows
API_INTERNAL_URL=http://${ODS_VIRTUAL_NAME}

ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1"

# IdP db keycloak
KEYCLOAK_DB_IMAGE_TAG=16.2
KEYCLOAK_POSTGRES_DB=keycloak_db
KEYCLOAK_POSTGRES_USER=edfi
KEYCLOAK_POSTGRES_PASSWORD=P@55w0rd
# IdP keycloak
KEYCLOAK_IMAGE_TAG=26.0
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_PORT=28080
KEYCLOAK_VIRTUAL_NAME=keycloak
KEYCLOAK_HOSTNAME= localhost
KEYCLOAK_HOSTNAME_PORT=443
KEYCLOAK_HOSTNAME_STRICT_BACKCHANNEL=false
KEYCLOAK_HTTP_ENABLED=true
KEYCLOAK_HOSTNAME_STRICT_HTTPS=true
KEYCLOAK_HEALTH_ENABLED=true
KEYCLOAK_ADMIN_CONSOLE_REALM=myrealm

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
API_MODE=SharedInstance
ODS_VIRTUAL_NAME=api
ADMIN_API_VIRTUAL_NAME=adminapi
LOGS_FOLDER=/tmp/logs

# For Authentication
ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME}
SIGNING_KEY=TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=

# For Postgres only
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
PGBOUNCER_LISTEN_PORT=6432

# The following needs to be set to specify the ODS API endpoint for Admin API to internally connect.
# If user chooses direct connection between ODS API and Admin API within docker network, then set the api internal url as follows
API_INTERNAL_URL=http://${ODS_VIRTUAL_NAME}

ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1"

# IdP db keycloak
KEYCLOAK_DB_IMAGE_TAG=16.2
KEYCLOAK_POSTGRES_DB=keycloak_db
KEYCLOAK_POSTGRES_USER=edfi
KEYCLOAK_POSTGRES_PASSWORD=P@55w0rd
# IdP keycloak
KEYCLOAK_IMAGE_TAG=26.0
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_PORT=28080
KEYCLOAK_VIRTUAL_NAME=keycloak
KEYCLOAK_HOSTNAME= localhost
KEYCLOAK_HOSTNAME_PORT=443
KEYCLOAK_HOSTNAME_STRICT_BACKCHANNEL=false
KEYCLOAK_HTTP_ENABLED=true
KEYCLOAK_HOSTNAME_STRICT_HTTPS=true
KEYCLOAK_HEALTH_ENABLED=true
KEYCLOAK_ADMIN_CONSOLE_REALM=myrealm

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using EdFi.Ods.AdminApi.Infrastructure.Documentation;
using EdFi.Ods.AdminApi.Infrastructure.Security;
using FluentValidation;
using Microsoft.AspNetCore;
Expand All @@ -17,6 +18,7 @@ namespace EdFi.Ods.AdminApi.Features.Connect;
[SwaggerResponse(400, FeatureConstants.BadRequestResponseDescription)]
[SwaggerResponse(500, FeatureConstants.InternalServerErrorResponseDescription)]
[Route(SecurityConstants.ConnectRoute)]
[SwaggerResponse(500, FeatureConstants.InternalServerErrorResponseDescription)]
public class ConnectController : Controller

{
Expand All @@ -33,6 +35,7 @@ public ConnectController(ITokenService tokenService, IRegisterService registerSe
[Consumes("application/x-www-form-urlencoded"), Produces("application/json")]
[SwaggerOperation("Registers new client", "Registers new client")]
[SwaggerResponse(200, "Application registered successfully.")]
[TypeFilter(typeof(FeatureToggleAttribute), Arguments = new object[] { "AppSettings:UseSelfcontainedAuthorization" })]
public async Task<IActionResult> Register([FromForm] RegisterService.RegisterClientRequest request)
{
if (await _registerService.Handle(request))
Expand All @@ -46,6 +49,7 @@ public async Task<IActionResult> Register([FromForm] RegisterService.RegisterCli
[Consumes("application/x-www-form-urlencoded"), Produces("application/json")]
[SwaggerOperation("Retrieves bearer token", "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here.")]
[SwaggerResponse(200, "Sign-in successful.")]
[TypeFilter(typeof(FeatureToggleAttribute), Arguments = new object[] { "AppSettings:UseSelfcontainedAuthorization" })]
public async Task<ActionResult> Token()
{
var request = HttpContext.GetOpenIddictServerRequest() ?? throw new ValidationException("Failed to parse token request");
Expand Down
33 changes: 33 additions & 0 deletions Application/EdFi.Ods.AdminApi/Features/FeatureToggleAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;

namespace EdFi.Ods.AdminApi.Features
{
public class FeatureToggleAttribute : ActionFilterAttribute, IFeatureToggleAttribute
{
private readonly string _featureName;
private readonly IConfiguration _configuration;

public FeatureToggleAttribute(string featureName, IConfiguration configuration)
{
_featureName = featureName;
_configuration = configuration;
}

public override void OnActionExecuting(ActionExecutingContext context)
{
var isDisabled = !_configuration.GetValue<bool>(_featureName);
if (isDisabled)
{
context.Result = new NotFoundResult();
}
base.OnActionExecuting(context);
}
}
}

14 changes: 14 additions & 0 deletions Application/EdFi.Ods.AdminApi/Features/IFeatureToggleAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using Microsoft.AspNetCore.Mvc.Filters;

namespace EdFi.Ods.AdminApi.Features
{
public interface IFeatureToggleAttribute
{
void OnActionExecuting(ActionExecutingContext context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public static AdminApiEndpointBuilder MapDelete(IEndpointRouteBuilder endpoints,
=> new(endpoints, HttpVerb.DELETE, route, handler);

public void BuildForVersions(params AdminApiVersions.AdminApiVersion[] versions)
{
BuildForVersions(string.Empty, versions);
}
public void BuildForVersions(string authorizationPolicy, params AdminApiVersions.AdminApiVersion[] versions)
{
if (versions.Length == 0)
throw new ArgumentException("Must register for at least 1 version");
Expand Down Expand Up @@ -74,7 +78,14 @@ public void BuildForVersions(params AdminApiVersions.AdminApiVersion[] versions)
}
else
{
builder.RequireAuthorization();
if (string.IsNullOrWhiteSpace(authorizationPolicy))
{
builder.RequireAuthorization(authorizationPolicy);
}
else
{
builder.RequireAuthorization();
}
}

builder.WithGroupName(version.ToString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace EdFi.Ods.AdminApi.Infrastructure.Documentation
{
public class SwaggerHideControllerConvention : IActionModelConvention
{
private readonly IConfiguration _configuration;
private readonly List<string> _controllerNames;

public SwaggerHideControllerConvention(IConfiguration configuration, List<string> controllerNames)
{
_configuration = configuration;
_controllerNames = controllerNames;
}

public void Apply(ActionModel action)
{
if (_controllerNames.Contains(action.Controller.ControllerType.Name))
{
var hideController = !_configuration.GetValue<bool>("AppSettings:UseSelfcontainedAuthorization");

if (hideController)
{
action.ApiExplorer.IsVisible = false;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// See the LICENSE and NOTICES files in the project root for more information.

using EdFi.Ods.AdminApi.Features.Connect;
using EdFi.Ods.AdminApi.Infrastructure.Documentation;
using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling;
using EdFi.Ods.AdminApi.Infrastructure.Extensions;
using Microsoft.AspNetCore.Authentication.JwtBearer;
Expand All @@ -23,7 +24,10 @@ public static void AddSecurityUsingOpenIddict(
IWebHostEnvironment webHostEnvironment
)
{
var issuer = configuration.Get<string>("Authentication:IssuerUrl");
bool useSelfContainedAuthorization = configuration.GetValue<bool>("AppSettings:UseSelfcontainedAuthorization");
var issuer = useSelfContainedAuthorization
? configuration.Get<string>("Authentication:IssuerUrl")
: configuration.Get<string>("Authentication:OIDC:Authority");
var isDockerEnvironment = configuration.Get<bool>("EnableDockerEnvironment");

//OpenIddict Server
Expand Down Expand Up @@ -81,12 +85,15 @@ IWebHostEnvironment webHostEnvironment
});

//Application Security
services.
var authenticationBuilder = services.
AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(opt =>
});
if (useSelfContainedAuthorization)
{
authenticationBuilder.AddJwtBearer(opt =>
{
opt.Authority = issuer;
opt.SaveToken = true;
Expand All @@ -100,17 +107,65 @@ IWebHostEnvironment webHostEnvironment
};
opt.RequireHttpsMetadata = !isDockerEnvironment;
});
}
else
{
authenticationBuilder.AddJwtBearer(options =>
{
var oidcIssuer = configuration.Get<string>("Authentication:OIDC:Authority");
if (!String.IsNullOrEmpty(oidcIssuer))
{
var oidcValidationCallback = configuration.Get<bool>("Authentication:OIDC:EnableServerCertificateCustomValidationCallback");
var requireHttpsMetadata = configuration.Get<bool>("Authentication:OIDC:RequireHttpsMetadata");
options.Authority = oidcIssuer;
options.SaveToken = true;
options.RequireHttpsMetadata = requireHttpsMetadata;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = configuration.Get<bool>("Authentication:OIDC:ValidateIssuer"),
ValidateIssuerSigningKey = false,
ValidIssuer = oidcIssuer,
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) =>
{
#pragma warning disable S4830 // Server certificates should be verified during SSL/TLS connections
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => oidcValidationCallback
};
#pragma warning restore S4830
// Server certificates should be verified during SSL/TLS connections
// Get public keys from keycloak
var client = new HttpClient(handler);
Console.WriteLine("Issuer" + oidcIssuer);
var response = client.GetStringAsync(oidcIssuer + "/protocol/openid-connect/certs").Result;
var keys = JsonWebKeySet.Create(response).GetSigningKeys();
return keys;
}
};
}
});
}


services.AddAuthorization(opt =>
{
opt.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireClaim(OpenIddictConstants.Claims.Scope, SecurityConstants.Scopes.AdminApiFullAccess)
.AddAuthenticationSchemes()
.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == OpenIddictConstants.Claims.Scope && c.Value.Contains(SecurityConstants.Scopes.AdminApiFullAccess))
)
.Build();
});

// Controllers to hide from Swagger conditionally
var controllerNamesToHide = new List<string> { "ConnectController" };
//Security Endpoints
services.AddTransient<ITokenService, TokenService>();
services.AddTransient<IRegisterService, RegisterService>();
services.AddControllers();
services.AddControllers(options =>
{
options.Conventions.Add(new SwaggerHideControllerConvention(configuration, controllerNamesToHide));
});
}

public class DefaultTokenResponseHandler : IOpenIddictServerHandler<ApplyTokenResponseContext>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@

using System.Reflection;
using EdFi.Admin.DataAccess.Contexts;
using EdFi.Ods.AdminApi.Infrastructure.Documentation;
using EdFi.Ods.AdminApi.Infrastructure.Security;
using EdFi.Common.Extensions;
using EdFi.Ods.AdminApi.Helpers;
using EdFi.Ods.AdminApi.Infrastructure.Api;
using EdFi.Ods.AdminApi.Infrastructure.Context;
using EdFi.Ods.AdminApi.Infrastructure.Documentation;
using EdFi.Ods.AdminApi.Infrastructure.Extensions;
using EdFi.Security.DataAccess.Contexts;
using EdFi.Ods.AdminApi.Infrastructure.MultiTenancy;
using EdFi.Ods.AdminApi.Infrastructure.Security;
using EdFi.Ods.AdminApi.Infrastructure.Services;
using EdFi.Security.DataAccess.Contexts;
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using FluentValidation;
using EdFi.Ods.AdminApi.Infrastructure.MultiTenancy;
using EdFi.Ods.AdminApi.Helpers;
using EdFi.Ods.AdminApi.Infrastructure.Context;
using EdFi.Common.Extensions;

namespace EdFi.Ods.AdminApi.Infrastructure;

Expand Down
12 changes: 10 additions & 2 deletions Application/EdFi.Ods.AdminApi/appsettings.Docker.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"DefaultPageSizeLimit": 25,
"DatabaseEngine": "PostgreSQL",
"PathBase": "$ADMIN_API_VIRTUAL_NAME",
"MultiTenancy": false
"MultiTenancy": false,
"EnableAdminConsoleAPI": "$ENABLE_ADMIN_CONSOLE",
"UseSelfcontainedAuthorization": "$USE_SELF_CONTAINED_AUTH"
},
"AdminConsole": {
"CorsSettings": {
Expand All @@ -18,7 +20,13 @@
"Authentication": {
"IssuerUrl": "$ISSUER_URL",
"SigningKey": "$SIGNING_KEY",
"AllowRegistration": true
"AllowRegistration": true,
"OIDC": {
"Authority": "$OIDC_AUTHORITY",
"ValidateIssuer": false,
"RequireHttpsMetadata": "$OIDC_REQUIRE_METADATA",
"EnableServerCertificateCustomValidationCallback": "$OIDC_ENABLE_SERVER_CERTIFICATE"
}
},
"SwaggerSettings": {
"EnableSwagger": true,
Expand Down
Loading
Loading