diff --git a/README.md b/README.md index 6850823..2804089 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,13 @@ There are two ways to provide configuration for supported scopes, clients and us * `API_RESOURCES_PATH` * `IDENTITY_RESOURCES_PATH` + +## Base path + +The server can be configured to run with base path. So all the server endpoints will be also available with some prefix segment. +For example `http://localhost:8080/my-base-path/.well-known/openid-configuration` and `http://localhost:8080/my-base-path/connect/token`. +Just set `BasePath` property in `ASPNET_SERVICES_OPTIONS_INLINE/PATH` env var. + ## Custom endpoints ### User management diff --git a/e2e/.env b/e2e/.env index 0a6334d..b96c24e 100644 --- a/e2e/.env +++ b/e2e/.env @@ -5,3 +5,5 @@ OIDC_USERINFO_URL=http://localhost:8080/connect/userinfo OIDC_GRANTS_URL=http://localhost:8080/grants OIDC_DISCOVERY_ENDPOINT_HTTPS=https://localhost:8443/.well-known/openid-configuration OIDC_MANAGE_USERS_URL=http://localhost:8080/api/v1/user +OIDC_DISCOVERY_ENDPOINT_WITH_BASE_PATH=http://localhost:8080/some-base-path/.well-known/openid-configuration +OIDC_TOKEN_URL_WITH_BASE_PATH=http://localhost:8080/some-base-path/connect/token diff --git a/e2e/.eslintrc b/e2e/.eslintrc index 536bb40..7ada225 100644 --- a/e2e/.eslintrc +++ b/e2e/.eslintrc @@ -31,7 +31,8 @@ "unicorn/numeric-separators-style": "off", "unicorn/prevent-abbreviations": ["error", { "checkFilenames": false - }] + }], + "unicorn/prefer-node-protocol": "off" }, "overrides": [ { diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 052de71..f916011 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -16,6 +16,10 @@ services: USERS_CONFIGURATION_PATH: /config/user-configuration.json CLIENTS_CONFIGURATION_PATH: /config/clients-configuration.json IDENTITY_RESOURCES_PATH: /config/identity-resources.json + ASPNET_SERVICES_OPTIONS_INLINE: | + { + "BasePath": "/some-base-path" + } volumes: - ./config:/config:ro - ./https:/https:ro diff --git a/e2e/tests/base-path.spec.ts b/e2e/tests/base-path.spec.ts new file mode 100644 index 0000000..563ba1c --- /dev/null +++ b/e2e/tests/base-path.spec.ts @@ -0,0 +1,33 @@ +import * as querystring from 'querystring'; +import * as dotenv from 'dotenv'; +import axios from 'axios'; + +import clients from '../config/clients-configuration.json'; +import type { Client } from '../types'; + +describe('Base path', () => { + let client: Client; + + beforeAll(() => { + dotenv.config(); + client = clients.find(c => c.ClientId === 'client-credentials-flow-client-id'); + expect(client).toBeDefined(); + }); + + test('Discovery Endpoint', async () => { + const response = await axios.get(process.env.OIDC_DISCOVERY_ENDPOINT_WITH_BASE_PATH); + expect(response.data.token_endpoint).toEqual(process.env.OIDC_TOKEN_URL_WITH_BASE_PATH); + }); + + test('Token Endpoint', async () => { + const parameters = { + client_id: client.ClientId, + client_secret: client.ClientSecrets?.[0], + grant_type: 'client_credentials', + scope: client.AllowedScopes.join(' '), + }; + + const response = await axios.post(process.env.OIDC_TOKEN_URL_WITH_BASE_PATH, querystring.stringify(parameters)); + expect(response).toBeDefined(); + }); +}); diff --git a/src/Helpers/AspNetServicesHelper.cs b/src/Helpers/AspNetServicesHelper.cs index 85803cb..27ca8e2 100644 --- a/src/Helpers/AspNetServicesHelper.cs +++ b/src/Helpers/AspNetServicesHelper.cs @@ -15,6 +15,8 @@ public class AspNetServicesOptions public SessionOptions Session { get; set; } public ForwardedHeadersOptions ForwardedHeadersOptions { get; set; } + + public string BasePath { get; set; } } public class AuthenticationOptions @@ -82,4 +84,4 @@ private static void ConfigureCors(IServiceCollection services, AspNetCorsOptions ); } } -} \ No newline at end of file +} diff --git a/src/Middlewares/BasePathMiddleware.cs b/src/Middlewares/BasePathMiddleware.cs new file mode 100644 index 0000000..9ac9522 --- /dev/null +++ b/src/Middlewares/BasePathMiddleware.cs @@ -0,0 +1,33 @@ +using IdentityServer4.Extensions; +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; +using IdentityServer4.Configuration; + +#pragma warning disable 1591 + +namespace OpenIdConnectServer.Middlewares +{ + public class BasePathMiddleware + { + private readonly RequestDelegate _next; + private readonly IdentityServerOptions _options; + + public BasePathMiddleware(RequestDelegate next, IdentityServerOptions options) + { + _next = next; + _options = options; + } + + public async Task Invoke(HttpContext context) + { + var basePath = Config.GetAspNetServicesOptions().BasePath; + var request = context.Request; + if(request.Path.Value.Length > basePath.Length) + { + request.Path = request.Path.Value.Substring(basePath.Length); + context.SetIdentityServerBasePath(basePath); + } + await _next(context); + } + } +} diff --git a/src/Startup.cs b/src/Startup.cs index c52ba1f..e32089a 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -6,10 +6,12 @@ using OpenIdConnectServer.Validation; using OpenIdConnectServer.JsonConverters; using Newtonsoft.Json.Serialization; +using OpenIdConnectServer.Middlewares; +using IdentityServer4.Hosting; namespace OpenIdConnectServer { - public class Startup + public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 @@ -52,6 +54,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) AspNetServicesHelper.UseAspNetServices(app, aspNetServicesOptions); app.UseIdentityServer(); + + var basePath = Config.GetAspNetServicesOptions().BasePath; + if (!string.IsNullOrEmpty(basePath)) + { + app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments(basePath), appBuilder => { + appBuilder.UseMiddleware(); + appBuilder.UseMiddleware(); + }); + } + app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization();