diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60d7e6eb..3a589147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: - hugo-version: '0.122.0' + hugo-version: '0.135.0' - name: Run hugo run: | echo $PWD diff --git a/IdentityModel/archetypes/default.md b/FOSS/archetypes/default.md similarity index 100% rename from IdentityModel/archetypes/default.md rename to FOSS/archetypes/default.md diff --git a/IdentityModel/cheatsheet.md b/FOSS/cheatsheet.md similarity index 100% rename from IdentityModel/cheatsheet.md rename to FOSS/cheatsheet.md diff --git a/IdentityModel/config.toml b/FOSS/config.toml similarity index 58% rename from IdentityModel/config.toml rename to FOSS/config.toml index fa74f414..6f9f64d8 100644 --- a/IdentityModel/config.toml +++ b/FOSS/config.toml @@ -1,6 +1,6 @@ -baseURL = "https://docs.duendesoftware.com/identitymodel" +baseURL = "https://docs.duendesoftware.com/foss" languageCode = "en-us" -title = "IdentityModel Documentation" +title = "Duende Open Source Documentation" theme = "hugo-theme-learn" # For search functionality @@ -8,4 +8,4 @@ theme = "hugo-theme-learn" home = [ "HTML", "RSS", "JSON"] [params] -editURL = "https://github.com/DuendeSoftware/docs.duendesoftware.com/edit/main/IdentityModel/docs/content/" \ No newline at end of file +editURL = "https://github.com/DuendeSoftware/docs.duendesoftware.com/edit/main/FOSS/docs/content/" \ No newline at end of file diff --git a/FOSS/content/AccessTokenManagement/Advanced/DPoP.md b/FOSS/content/AccessTokenManagement/Advanced/DPoP.md new file mode 100644 index 00000000..f4f6f690 --- /dev/null +++ b/FOSS/content/AccessTokenManagement/Advanced/DPoP.md @@ -0,0 +1,70 @@ ++++ +title = "DPop" +weight = 40 +chapter = false ++++ + +[DPoP](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop) specifies how to bind an asymmetric key stored within a JSON Web Key (JWK) to an access token. This will make the access token bound to the key such that if the access token were to leak, it cannot be used without also having access to the private key of the corresponding JWK. + +The "Duende.AccessTokenManagement" library supports DPoP. + +## DPoP Key + +The main piece that your hosting application needs to concern itself with is how to obtain (and manage) the DPoP key. This key (and signing algorithm) will be either a "RS", "PS", or "ES" style key, and needs to be in the form of a JSON Web Key (or JWK). Consult the specification for more details. + +The creation and management of this DPoP key is up to the policy of the client. For example is can be dynamically created when the client starts up, and can be periodically rotated. The main constraint is that it must be stored for as long as the client uses any access tokens (and possibly refresh tokens) that they are bound to, which this library will manage for you. + +Creating a JWK in .NET is simple: + +```cs +var rsaKey = new RsaSecurityKey(RSA.Create(2048)); +var jwkKey = JsonWebKeyConverter.ConvertFromSecurityKey(rsaKey); +jwkKey.Alg = "PS256"; +var jwk = JsonSerializer.Serialize(jwkKey); +``` + +## Key Configuration + +Once you have a JWK you wish to use, then it must be configured or made available to this library. That can be done in one of two ways: + +* Configure the key at startup by setting the `DPoPJsonWebKey` property on either the `ClientCredentialsTokenManagementOptions` or `UserTokenManagementOptions` (depending on which of the two styles you are using from this library). +* Implement the `IDPoPKeyStore` interface to produce the key at runtime. + +Here's a sample configuring the key in an application using `AddOpenIdConnectAccessTokenManagement` in the startup code: + +```cs +services.AddOpenIdConnectAccessTokenManagement(options => +{ + options.DPoPJsonWebKey = jwk; +}); +``` + +Similarly for an application using `AddClientCredentialsTokenManagement` it would look like this: + +```cs +services.AddClientCredentialsTokenManagement() + .AddClient("client_name", options => + { + options.DPoPJsonWebKey = jwk; + }); +``` + +## Proof Tokens at the token server's token endpoint + +Once the key has been configured for the client, then the library will use it to produce a DPoP proof token when calling the token server (including token renewals if relevant). +There is nothing explicit needed on behalf of the developer using this library. + +### dpop_jkt at the token server's authorize endpoint + +When using DPoP and `AddOpenIdConnectAccessTokenManagement`, this library will also automatically include the `dpop_jkt` parameter to the authorize endpoint. + +## Proof Tokens at the API + +Once the library has obtained a DPoP bound access token for the client, then if your application is using any of the `HttpClient` client factory helpers (e.g. `AddClientCredentialsHttpClient` or `AddUserAccessTokenHttpClient`) then those outbound HTTP requests will automatically include a DPoP proof token for the associated DPoP access token. + +## Considerations + +A point to keep in mind when using DPoP and `AddOpenIdConnectAccessTokenManagement` is that the DPoP proof key is created per user session. +This proof key must be store somewhere, and the `AuthenticationProperties` used by both the OIDC and cookie handlers is what is used to store this key. +This implies that the OIDC `state` parameter will increase in size, as well the resultant cookie that represents the user's session. +The storage of each of these can be customized with the properties on the options `StateDataFormat` and `SessionStore` respectively. diff --git a/FOSS/content/AccessTokenManagement/Advanced/_index.md b/FOSS/content/AccessTokenManagement/Advanced/_index.md new file mode 100644 index 00000000..b3ffee61 --- /dev/null +++ b/FOSS/content/AccessTokenManagement/Advanced/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Advanced" +weight = 50 +chapter = true ++++ + +Advanced +======== diff --git a/FOSS/content/AccessTokenManagement/Advanced/client_assertions.md b/FOSS/content/AccessTokenManagement/Advanced/client_assertions.md new file mode 100644 index 00000000..614ac388 --- /dev/null +++ b/FOSS/content/AccessTokenManagement/Advanced/client_assertions.md @@ -0,0 +1,60 @@ ++++ +title = "Client Assertions" +weight = 30 +chapter = false ++++ + +If your token client is using a client assertion instead of a shared secret, you can provide the assertion in two ways + +* use the request parameter mechanism to pass a client assertion to the management +* implement the `IClientAssertionService` interface to centralize client assertion creation + +Here's a sample client assertion service using the Microsoft JWT library: + +```cs +public class ClientAssertionService : IClientAssertionService +{ + private readonly IOptionsSnapshot _options; + + public ClientAssertionService(IOptionsSnapshot options) + { + _options = options; + } + + public Task GetClientAssertionAsync( + string? clientName = null, TokenRequestParameters? parameters = null) + { + if (clientName == "invoice") + { + var options = _options.Get(clientName); + + var descriptor = new SecurityTokenDescriptor + { + Issuer = options.ClientId, + Audience = options.TokenEndpoint, + Expires = DateTime.UtcNow.AddMinutes(1), + SigningCredentials = GetSigningCredential(), + + Claims = new Dictionary + { + { JwtClaimTypes.JwtId, Guid.NewGuid().ToString() }, + { JwtClaimTypes.Subject, options.ClientId! }, + { JwtClaimTypes.IssuedAt, DateTime.UtcNow.ToEpochTime() } + } + }; + + var handler = new JsonWebTokenHandler(); + var jwt = handler.CreateToken(descriptor); + + return Task.FromResult(new ClientAssertion + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = jwt + }); + } + + return Task.FromResult(null); + } +} +``` + diff --git a/FOSS/content/AccessTokenManagement/Advanced/client_credentials.md b/FOSS/content/AccessTokenManagement/Advanced/client_credentials.md new file mode 100644 index 00000000..eca49a6a --- /dev/null +++ b/FOSS/content/AccessTokenManagement/Advanced/client_credentials.md @@ -0,0 +1,124 @@ ++++ +title = "Customizing Client Credentials Token Management" +weight = 10 +chapter = false ++++ + +The most common way to use the access token management for machine to machine communication is described [here](worker-applications) - however you may want to customize certain aspects of it - here's what you can do. + +### Client options + +You can add token client definitions to your host while configuring the DI container, e.g.: + +```cs +services.AddClientCredentialsTokenManagement() + .AddClient("invoices", client => + { + client.TokenEndpoint = "https://sts.company.com/connect/token"; + + client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; + client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c"; + client.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader; + + client.Scope = "list"; + client.Resource = "urn:invoices"; + }) +``` + +You can set the following options: + +* `TokenEndpoint` - URL of the OAuth token endpoint where this token client requests tokens from +* `ClientId` - client ID +* `ClientSecret` - client secret (if a shared secret is used) +* `ClientCredentialStyle` - Specifies how the client ID / secret is sent to the token endpoint. Options are using using the authorization header, or POST body values (defaults to header) +* `Scope` - the requested scope of access (if any) +* `Resource` - the resource indicator (if any) + +Internally the standard .NET options system is used to register the configuration. This means you can also register clients like this: + +```cs +services.Configure("invoices", client => +{ + client.TokenEndpoint = "https://sts.company.com/connect/token"; + + client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; + client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c"; + + client.Scope = "list"; + client.Resource = "urn:invoices"; +}); +``` + +Or use the `IConfigureNamedOptions` if you need access to the DI container during registration, e.g.: + +```cs +public class ClientCredentialsClientConfigureOptions : IConfigureNamedOptions +{ + private readonly DiscoveryCache _cache; + + public ClientCredentialsClientConfigureOptions(DiscoveryCache cache) + { + _cache = cache; + } + + public void Configure(string name, ClientCredentialsClient options) + { + if (name == "invoices") + { + var disco = _cache.GetAsync().GetAwaiter().GetResult(); + + options.TokenEndpoint = disco.TokenEndpoint; + + client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; + client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c"; + + client.Scope = "list"; + client.Resource = "urn:invoices"; + } + } +} +``` + +..and register the config options e.g. like this: + +```cs +services.AddClientCredentialsTokenManagement(); + +services.AddSingleton(new DiscoveryCache("https://sts.company.com")); +services.AddSingleton, + ClientCredentialsClientConfigureOptions>(); +``` + +#### Backchannel communication + +By default all backchannel communication will be done using a named client from the HTTP client factory. The name is `Duende.AccessTokenManagement.BackChannelHttpClient` which is also a constant called `ClientCredentialsTokenManagementDefaults.BackChannelHttpClientName`. + +You can register your own HTTP client with the factory using the above name and thus provide your own custom HTTP client. + +The client registration object has two additional properties to customize the HTTP client: + +* `HttpClientName` - if set, this HTTP client name from the factory will be used instead of the default one +* `HttpClient` - allows setting an instance of `HttpClient` to use. Will take precedence over a client name + +### Token caching + +By default, tokens will be cached using the `IDistributedCache` abstraction in ASP.NET Core. You can either use the in-memory cache version, or plugin a real distributed cache like Redis. + +```cs +services.AddDistributedMemoryCache(); +``` + +The built-in cache uses two settings from the options: + +```cs +services.AddClientCredentialsTokenManagement(options => + { + options.CacheLifetimeBuffer = 60; + options.CacheKeyPrefix = "Duende.AccessTokenManagement.Cache::"; + }); +``` + +`CacheLifetimeBuffer` is a value in seconds that will be subtracted from the token lifetime, e.g. if a token is valid for one hour, it will be cached for 59 minutes only. The cache key prefix is used to construct the unique key for the cache item based on client name, requested scopes and resource. + +Finally, you can also replace the caching implementation altogether by registering your own `IClientCredentialsTokenCache`. + diff --git a/FOSS/content/AccessTokenManagement/Advanced/user_tokens.md b/FOSS/content/AccessTokenManagement/Advanced/user_tokens.md new file mode 100644 index 00000000..d9d63b2b --- /dev/null +++ b/FOSS/content/AccessTokenManagement/Advanced/user_tokens.md @@ -0,0 +1,86 @@ ++++ +title = "Customizing User Token Management" +weight = 20 +chapter = false ++++ + +The most common way to use the access token management for interactive web applications is described [here](web-applications) - however you may want to customise certain aspects of it - here's what you can do. + +### General options +You can pass in some global options when registering token management in DI. + +* `ChallengeScheme` - by default the OIDC configuration is inferred from the default challenge scheme. This is recommended approach. If for some reason your OIDC handler is not the default challenge scheme, you can set the scheme name on the options +* `UseChallengeSchemeScopedTokens` - the general assumption is that you only have one OIDC handler configured. If that is not the case, token management needs to maintain multiple sets of token artefacts simultaneously. You can opt-in to that feature using this setting. +* `ClientCredentialsScope` - when requesting client credentials tokens from the OIDC provider, the scope parameter will not be set since its value cannot be inferred from the OIDC configuration. With this setting you can set the value of the scope parameter. +* `ClientCredentialsResource` - same as previous, but for the resource parameter +* `ClientCredentialStyle` - specifies how client credentials are transmitted to the OIDC provider + +```cs +builder.Services.AddOpenIdConnectAccessTokenManagement(options => +{ + options.ChallengeScheme = "schmeName"; + options.UseChallengeSchemeScopedTokens = false; + + options.ClientCredentialsScope = "api1 api2"; + options.ClientCredentialsResource = "urn:resource"; + options.ClientCredentialStyle = ClientCredentialStyle.PostBody; +}); +``` + + + +### Per request parameters + +You can also modify token management parameters on a per-request basis. + +The `UserTokenRequestParameters` class can be used for that: + +* `SignInScheme` - allows specifying a sign-in scheme. This is used by the default token store +* `ChallengeScheme` - allows specifying a challenge scheme. This is used to infer token service configuration +* `ForceRenewal` - forces token retrieval even if a cached token would be available +* `Scope` - overrides the globally configured scope parameter +* `Resource` - override the globally configured resource parameter +* `Assertion` - allows setting a client assertion for the request + +The request parameters can be passed via the manual API + +```cs +var token = await _tokenManagementService.GetAccessTokenAsync(User, new UserAccessTokenRequestParameters { ... }); +``` + +..the extension methods + +```cs +var token = await HttpContext.GetUserAccessTokenAsync( + new UserTokenRequestParameters { ... }); +``` + +...or the HTTP client factory + +```cs +// registers HTTP client that uses the managed user access token +builder.Services.AddUserAccessTokenHttpClient("invoices", + parameters: new UserTokenRequestParameters { ... }, + configureClient: client => + { + client.BaseAddress = new Uri("https://api.company.com/invoices/"); + }); + +// registers a typed HTTP client with token management support +builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://api.company.com/invoices/"); + }) + .AddUserAccessTokenHandler(new UserTokenRequestParameters { ... }); +``` + + + +### Token storage + +By default the user's access and refresh token will be store in the ASP.NET Core autentication session (implemented by the cookie handler). + +You can modify this in two ways + +* the cookie handler itself has an extensible storage mechanism via the `TicketStore` mechanism +* replace the store altogether by providing an `IUserTokenStore` implementation diff --git a/FOSS/content/AccessTokenManagement/_index.md b/FOSS/content/AccessTokenManagement/_index.md new file mode 100644 index 00000000..3175d69d --- /dev/null +++ b/FOSS/content/AccessTokenManagement/_index.md @@ -0,0 +1,14 @@ ++++ +title = "AccessTokenManagement" +weight = 10 +chapter = true ++++ + +AccessTokenManagement +======== + +This library provides automatic access token management features for .NET worker and ASP.NET Core web applications + +* automatic acquisition and lifetime management of client credentials based access tokens for machine to machine communication +* automatic access token lifetime management using a refresh token for API calls on-behalf of the currently logged-in user +* revocation of access tokens \ No newline at end of file diff --git a/FOSS/content/AccessTokenManagement/blazor_server.md b/FOSS/content/AccessTokenManagement/blazor_server.md new file mode 100644 index 00000000..84ce1039 --- /dev/null +++ b/FOSS/content/AccessTokenManagement/blazor_server.md @@ -0,0 +1,73 @@ ++++ +title = "Blazor Server" +weight = 30 +chapter = false ++++ + +## Overview + +Blazor Server applications have the same token management requirements as a regular ASP.NET Core web application. Because Blazor Server streams content to the application over a websocket, there often is no HTTP request or response to interact with during the execution of a Blazor Server application. You therefore cannot use `HttpContext` in a Blazor Server application as you would in a traditional ASP.NET Core web application. + +This means: + +* you cannot use `HttpContext` extension methods +* you cannot use the ASP.NET authentication session to store tokens +* the normal mechanism used to automatically attach tokens to Http Clients making API calls won't work + +Fortunately, Duende.AccessTokenManagement provides a straightforward solution to these problems. Also see the `BlazorServer` sample for source code of a full example. + +### Token storage + +Since the tokens cannot be managed in the authentication session, you need to store them somewhere else. The options include an in-memory data structure, a distributed cache like redis, or a database. Duende.AccessTokenManagement describes this store for tokens with the `IUserTokenStore` interface. In non-blazor scenarios, the default implementation that stores the tokens in the session is used. In your Blazor server application, you'll need to decide where you want to store the tokens and implement the store interface. + +The store interface is very simple. `StoreTokenAsync` adds a token to the store for a particular user, `GetTokenAsync` retrieves the user's token, and `ClearTokenAsync` clears the tokens stored for a particular user. A sample implementation that stores the tokens in memory can be found in the `ServerSideTokenStore` in the `BlazorServer` sample. + +Register your token store in the DI container and tell Duende.AccessTokenManagement to integrate with Blazor by calling `AddBlazorServerAccessTokenManagement`: + +``` +builder.Services.AddOpenIdConnectAccessTokenManagement() + .AddBlazorServerAccessTokenManagement(); +``` + +Once you've registered your token store, you need to use it. You initialize the token store with the `TokenValidated` event in the OpenID Connect handler: + +```cs +public class OidcEvents : OpenIdConnectEvents +{ + private readonly IUserTokenStore _store; + + public OidcEvents(IUserTokenStore store) + { + _store = store; + } + + public override async Task TokenValidated(TokenValidatedContext context) + { + var exp = DateTimeOffset.UtcNow.AddSeconds(Double.Parse(context.TokenEndpointResponse!.ExpiresIn)); + + await _store.StoreTokenAsync(context.Principal!, new UserToken + { + AccessToken = context.TokenEndpointResponse.AccessToken, + AccessTokenType = context.TokenEndpointResponse.TokenType, + Expiration = exp, + RefreshToken = context.TokenEndpointResponse.RefreshToken, + Scope = context.TokenEndpointResponse.Scope + }); + + await base.TokenValidated(context); + } +} +``` + +Once registered and initialized, Duende.AccessTokenManagement will keep the store up to date automatically as tokens are refreshed. + +### Retrieving and using tokens + +If you've registered your token store with `AddBlazorServerAccessTokenManagement`, Duende.AccessTokenManagement will register the services necessary to attach tokens to outgoing HTTP requests automatically, using the same API as a non-blazor application. You inject an HTTP client factory and resolve named HTTP clients where ever you need to make HTTP requests, and you register the HTTP client's that use access tokens in the DI system with our extension method: + +``` +builder.Services.AddUserAccessTokenHttpClient("demoApiClient", configureClient: client => +{ + client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/"); +}); +``` \ No newline at end of file diff --git a/FOSS/content/AccessTokenManagement/web_apps.md b/FOSS/content/AccessTokenManagement/web_apps.md new file mode 100644 index 00000000..13acf841 --- /dev/null +++ b/FOSS/content/AccessTokenManagement/web_apps.md @@ -0,0 +1,200 @@ ++++ +title = "Web Applications" +weight = 20 +chapter = false ++++ + +## Overview + +This library automates all the tasks around access token lifetime management for user-centric web applications. + +While many of the details can be customized, by default the following is assumed: + +* ASP.NET Core web application +* cookie authentication handler for session management +* OpenID Connect authentication handler for authentication and access token requests against an OpenID Connect compliant token service +* the token service returns a refresh token + +### Setup + +By default, the token management library will use the ASP.NET Core default authentication scheme for token storage (this is typically the cookie handler and its authentication session), and the default challenge scheme for deriving token client configuration for refreshing tokens or requesting client credential tokens (this is typically the OpenID Connect handler pointing to your trusted authority). + +```cs +// setting up default schemes and handlers +builder.Services.AddAuthentication(options => + { + options.DefaultScheme = "cookie"; + options.DefaultChallengeScheme = "oidc"; + }) + .AddCookie("cookie", options => + { + options.Cookie.Name = "web"; + + // automatically revoke refresh token at signout time + options.Events.OnSigningOut = async e => { await e.HttpContext.RevokeRefreshTokenAsync(); }; + }) + .AddOpenIdConnect("oidc", options => + { + options.Authority = "https://sts.company.com"; + + options.ClientId = "webapp"; + options.ClientSecret = "secret"; + + options.ResponseType = "code"; + options.ResponseMode = "query"; + + options.Scope.Clear(); + + // OIDC related scopes + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("email"); + + // API scopes + options.Scope.Add("invoice"); + options.Scope.Add("customer"); + + // requests a refresh token + options.Scope.Add("offline_access"); + + options.GetClaimsFromUserInfoEndpoint = true; + options.MapInboundClaims = false; + + // important! this store the access and refresh token in the authentication session + // this is needed to the standard token store to manage the artefacts + options.SaveTokens = true; + + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "name", + RoleClaimType = "role" + }; + }); + +// adds services for token management +builder.Services.AddOpenIdConnectAccessTokenManagement(); + +``` + +#### HTTP client factory + +Similar to the worker service support, you can register HTTP clients that automatically send the access token of the current user when making API calls. The message handler plumbing associated with those HTTP clients will try to make sure, the access token is always valid and not expired. + +```cs +// registers HTTP client that uses the managed user access token +builder.Services.AddUserAccessTokenHttpClient("invoices", + configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); }); +``` + +This could be also a typed client: + +```cs +// registers a typed HTTP client with token management support +builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://api.company.com/invoices/"); + }) + .AddUserAccessTokenHandler(); +``` + +Of course, the ASP.NET Core web application host could also do machine to machine API calls that are independent of a user. In this case all the token client configuration can be inferred from the OpenID Connect handler configuration. The following registers an HTTP client that uses a client credentials token for outgoing calls: + +```cs +// registers HTTP client that uses the managed client access token +builder.Services.AddClientAccessTokenHttpClient("masterdata.client", + configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/masterdata/"); }); +``` + +..and as a typed client: + +```cs +builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://api.company.com/masterdata/"); + }) + .AddClientAccessTokenHandler(); +``` + +### Usage + +There are three ways to interact with the token management service: + +* manually +* HTTP context extension methods +* HTTP client factory + +#### Manually + +You can get the current user and client access token manually by writing code against the `IUserTokenManagementService`. + +```cs +public class HomeController : Controller +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IUserTokenManagementService _tokenManagementService; + + public HomeController(IHttpClientFactory httpClientFactory, IUserTokenManagementService tokenManagementService) + { + _httpClientFactory = httpClientFactory; + _tokenManagementService = tokenManagementService; + } + + public async Task CallApi() + { + var token = await _tokenManagementService.GetAccessTokenAsync(User); + var client = _httpClientFactory.CreateClient(); + client.SetBearerToken(token.Value); + + var response = await client.GetAsync("https://api.company.com/invoices"); + + // rest omitted + } +} +``` + +#### HTTP context extension methods + +There are three extension methods on the HTTP context that simplify interaction with the token management service: + +* `GetUserAccessTokenAsync` - returns an access token representing the user. If the current access token is expired, it will be refreshed. +* `GetClientAccessTokenAsync` - returns an access token representing the client. If the current access token is expired, a new one will be requested +* `RevokeRefreshTokenAsync` - revokes the refresh token + +```cs +public async Task CallApi() +{ + var token = await HttpContext.GetUserAccessTokenAsync(); + var client = _httpClientFactory.CreateClient(); + client.SetBearerToken(token.Value); + + var response = await client.GetAsync("https://api.company.com/invoices"); + + // rest omitted +} +``` + +#### HTTP client factory + +Last but not least, if you registered clients with the factory, you can simply use them. They will try to make sure that a current access token is always sent along. If that is not possible, ultimately a 401 will be returned to the calling code. + +```cs +public async Task CallApi() +{ + var client = _httpClientFactory.CreateClient("invoices"); + + var response = await client.GetAsync("list"); + + // rest omitted +} +``` + +...or for a typed client: + +```cs +public async Task CallApi([FromServices] InvoiceClient client) +{ + var response = await client.GetList(); + + // rest omitted +} +``` \ No newline at end of file diff --git a/FOSS/content/AccessTokenManagement/workers.md b/FOSS/content/AccessTokenManagement/workers.md new file mode 100644 index 00000000..d61427e6 --- /dev/null +++ b/FOSS/content/AccessTokenManagement/workers.md @@ -0,0 +1,141 @@ ++++ +title = "Workers" +weight = 10 +chapter = false ++++ + +## Overview + +A common scenario in worker applications or background tasks (or really any demon-style applications) is to call APIs using an OAuth token obtained via the client credentials flow. + +The access tokens need to be requested and cached (either locally or shared between multiple instances) and made available to the code calling the APIs. In case of expiration (or other token invalidation reasons), a new access token needs to be requested. + +The actual business code should not need to be aware of any of this. + +Have a look for the `Worker` project in the samples folder for running code. + +### Setup + +Start by adding a reference to the `Duende.AccessTokenManagement` Nuget package to your application. + +You can add the necessary services to the DI system by calling `AddClientCredentialsTokenManagement()`. After that you can add one or more named client definitions by calling `AddClient`. + +```cs +// default cache +services.AddDistributedMemoryCache(); + +services.AddClientCredentialsTokenManagement() + .AddClient("catalog.client", client => + { + client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token"; + + client.ClientId = "6f59b670-990f-4ef7-856f-0dd584ed1fac"; + client.ClientSecret = "d0c17c6a-ba47-4654-a874-f6d576cdf799"; + + client.Scope = "catalog inventory"; + }) + .AddClient("invoice.client", client => + { + client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token"; + + client.ClientId = "ff8ac57f-5ade-47f1-b8cd-4c2424672351"; + client.ClientSecret = "4dbbf8ec-d62a-4639-b0db-aa5357a0cf46"; + + client.Scope = "invoice customers"; + }); +``` + +#### HTTP Client Factory + +You can register HTTP clients with the factory that will automatically use the above client definitions to request and use access tokens. + +The following code registers an HTTP client called `invoices` to automatically use the `invoice.client` definition: + +```cs +services.AddClientCredentialsHttpClient("invoices", "invoice.client", client => +{ + client.BaseAddress = new Uri("https://apis.company.com/invoice/"); +}); +``` + +You can also setup a typed HTTP client to use a token client definition, e.g.: + +```cs +services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://apis.company.com/catalog/"); + }) + .AddClientCredentialsTokenHandler("catalog.client"); +``` + +### Usage + +There are two fundamental ways to interact with token management - manually, or via the HTTP factory. + +#### Manual + +You can retrieve the current access token for a given token client via `IClientCredentialsTokenManagementService.GetAccessTokenAsync`. + +```cs +public class WorkerManual : BackgroundService +{ + private readonly IHttpClientFactory _clientFactory; + private readonly IClientCredentialsTokenManagementService _tokenManagementService; + + public WorkerManualIHttpClientFactory factory, IClientCredentialsTokenManagementService tokenManagementService) + { + _clientFactory = factory; + _tokenManagementService = tokenManagementService; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var client = _clientFactory.CreateClient(); + client.BaseAddress = new Uri("https://apis.company.com/catalog/"); + + // get access token for client and set on HttpClient + var token = await _tokenManagementService.GetAccessTokenAsync("catalog.client"); + client.SetBearerToken(token.Value); + + var response = await client.GetAsync("list", stoppingToken); + + // rest omitted + } + } +} +``` + +You can customize some of the per-request parameters by passing in an instance of `ClientCredentialsTokenRequestParameters`. This allows forcing a fresh token request (even if a cached token would exist) and also allows setting a per request scope, resource and client assertion. + +#### HTTP factory + +If you have setup HTTP clients in the HTTP factory, then no token related code is needed at all, e.g.: + +```cs +public class WorkerHttpClient : BackgroundService +{ + private readonly ILogger _logger; + private readonly IHttpClientFactory _clientFactory; + + public WorkerHttpClient(ILogger logger, IHttpClientFactory factory) + { + _logger = logger; + _clientFactory = factory; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var client = _clientFactory.CreateClient("invoices"); + var response = await client.GetAsync("test", stoppingToken); + + // rest omitted + } + } +} +``` + +**remark** The clients in the factory have a message handler attached to them that automatically re-tries the request in case of a 401 response code. The request get re-sent with a newly requested access token. If this still results in a 401, the response is returned to the caller. \ No newline at end of file diff --git a/IdentityModel/content/native/_index.md b/FOSS/content/IdentityModel.OidcClient/_index.md similarity index 81% rename from IdentityModel/content/native/_index.md rename to FOSS/content/IdentityModel.OidcClient/_index.md index 93692c46..4212f238 100644 --- a/IdentityModel/content/native/_index.md +++ b/FOSS/content/IdentityModel.OidcClient/_index.md @@ -1,13 +1,13 @@ +++ -title = "Mobile/Native Applications" +title = "IdentityModel.OidcClient" weight = 30 chapter = true +++ -OpenIdConnect for Native Applications +Duende.IdentityModel.OidcClient ======== -IdentityModel.OidcClient is an OpenID Connect (OIDC) client library for mobile and native +Duende.IdentityModel.OidcClient is an OpenID Connect (OIDC) client library for mobile and native applications in .NET. It is a certified OIDC relying party and implements [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252/), "OAuth 2.0 for native Applications". It provides types that describe OIDC requests and responses, low level diff --git a/IdentityModel/content/native/automatic.md b/FOSS/content/IdentityModel.OidcClient/automatic.md similarity index 100% rename from IdentityModel/content/native/automatic.md rename to FOSS/content/IdentityModel.OidcClient/automatic.md diff --git a/IdentityModel/content/native/logging.md b/FOSS/content/IdentityModel.OidcClient/logging.md similarity index 100% rename from IdentityModel/content/native/logging.md rename to FOSS/content/IdentityModel.OidcClient/logging.md diff --git a/IdentityModel/content/native/manual.md b/FOSS/content/IdentityModel.OidcClient/manual.md similarity index 100% rename from IdentityModel/content/native/manual.md rename to FOSS/content/IdentityModel.OidcClient/manual.md diff --git a/IdentityModel/content/native/samples.md b/FOSS/content/IdentityModel.OidcClient/samples.md similarity index 100% rename from IdentityModel/content/native/samples.md rename to FOSS/content/IdentityModel.OidcClient/samples.md diff --git a/FOSS/content/IdentityModel/_index.md b/FOSS/content/IdentityModel/_index.md new file mode 100644 index 00000000..18608cdc --- /dev/null +++ b/FOSS/content/IdentityModel/_index.md @@ -0,0 +1,8 @@ ++++ +title = "IdentityModel" +weight = 20 +chapter = true ++++ + +IdentityModel +======== diff --git a/IdentityModel/content/endpoints/_index.md b/FOSS/content/IdentityModel/endpoints/_index.md similarity index 100% rename from IdentityModel/content/endpoints/_index.md rename to FOSS/content/IdentityModel/endpoints/_index.md diff --git a/IdentityModel/content/endpoints/device_authorize.md b/FOSS/content/IdentityModel/endpoints/device_authorize.md similarity index 100% rename from IdentityModel/content/endpoints/device_authorize.md rename to FOSS/content/IdentityModel/endpoints/device_authorize.md diff --git a/IdentityModel/content/endpoints/discovery.md b/FOSS/content/IdentityModel/endpoints/discovery.md similarity index 100% rename from IdentityModel/content/endpoints/discovery.md rename to FOSS/content/IdentityModel/endpoints/discovery.md diff --git a/IdentityModel/content/endpoints/dynamic_registration.md b/FOSS/content/IdentityModel/endpoints/dynamic_registration.md similarity index 100% rename from IdentityModel/content/endpoints/dynamic_registration.md rename to FOSS/content/IdentityModel/endpoints/dynamic_registration.md diff --git a/IdentityModel/content/endpoints/introspection.md b/FOSS/content/IdentityModel/endpoints/introspection.md similarity index 100% rename from IdentityModel/content/endpoints/introspection.md rename to FOSS/content/IdentityModel/endpoints/introspection.md diff --git a/IdentityModel/content/endpoints/revocation.md b/FOSS/content/IdentityModel/endpoints/revocation.md similarity index 100% rename from IdentityModel/content/endpoints/revocation.md rename to FOSS/content/IdentityModel/endpoints/revocation.md diff --git a/IdentityModel/content/endpoints/token.md b/FOSS/content/IdentityModel/endpoints/token.md similarity index 100% rename from IdentityModel/content/endpoints/token.md rename to FOSS/content/IdentityModel/endpoints/token.md diff --git a/IdentityModel/content/endpoints/userinfo.md b/FOSS/content/IdentityModel/endpoints/userinfo.md similarity index 100% rename from IdentityModel/content/endpoints/userinfo.md rename to FOSS/content/IdentityModel/endpoints/userinfo.md diff --git a/IdentityModel/content/utils/_index.md b/FOSS/content/IdentityModel/utils/_index.md similarity index 100% rename from IdentityModel/content/utils/_index.md rename to FOSS/content/IdentityModel/utils/_index.md diff --git a/IdentityModel/content/utils/base64.md b/FOSS/content/IdentityModel/utils/base64.md similarity index 100% rename from IdentityModel/content/utils/base64.md rename to FOSS/content/IdentityModel/utils/base64.md diff --git a/IdentityModel/content/utils/constants.md b/FOSS/content/IdentityModel/utils/constants.md similarity index 100% rename from IdentityModel/content/utils/constants.md rename to FOSS/content/IdentityModel/utils/constants.md diff --git a/IdentityModel/content/utils/epoch_time.md b/FOSS/content/IdentityModel/utils/epoch_time.md similarity index 100% rename from IdentityModel/content/utils/epoch_time.md rename to FOSS/content/IdentityModel/utils/epoch_time.md diff --git a/IdentityModel/content/utils/request_url.md b/FOSS/content/IdentityModel/utils/request_url.md similarity index 100% rename from IdentityModel/content/utils/request_url.md rename to FOSS/content/IdentityModel/utils/request_url.md diff --git a/IdentityModel/content/utils/time_constant_comparison.md b/FOSS/content/IdentityModel/utils/time_constant_comparison.md similarity index 100% rename from IdentityModel/content/utils/time_constant_comparison.md rename to FOSS/content/IdentityModel/utils/time_constant_comparison.md diff --git a/IdentityModel/content/utils/x509store.md b/FOSS/content/IdentityModel/utils/x509store.md similarity index 100% rename from IdentityModel/content/utils/x509store.md rename to FOSS/content/IdentityModel/utils/x509store.md diff --git a/FOSS/content/_index.md b/FOSS/content/_index.md new file mode 100644 index 00000000..a8381f9f --- /dev/null +++ b/FOSS/content/_index.md @@ -0,0 +1,43 @@ ++++ +title = "Welcome to Duende Open Source" +weight = 10 +chapter = true ++++ + + +Welcome to Duende Open Source +======================== + +Duende Software sponsors a series of free open source libraries under the Apache 2 license. + +These libraries help with building OAuth 2.0 and OpenID Connect clients. + +## Duende.AccessTokenManagement + +A set of .NET libraries that manage OAuth and OpenId Connect access tokens. These tools automatically acquire new tokens when old tokens are about to expire, provide conveniences for using the current token with HTTP clients, and can revoke tokens that are no longer needed. + +## Duende.IdentityModel + +The Duende.IdentityModel package is the base library for OIDC and OAuth 2.0 related protocol +operations. It provides an object model to interact with the endpoints defined in the +various OAuth and OpenId Connect specifications in the form of types to represent the +requests and responses, extension methods to invoke requests constants defined in the +specifications, such as standard scope, claim, and parameter names, and other convenience +methods for performing common identity related operations + +Duende.IdentityModel targets .NET Standard 2.0, making it suitable for .NET and .NET Framework. + +- GitHub: +- NuGet: + +## Duende.IdentityModel.OidcClient + +Duende.IdentityModel.OidcClient is an OpenID Connect (OIDC) client library for native +applications in .NET. It is a certified OIDC relying party and implements [RFC +8252](https://datatracker.ietf.org/doc/html/rfc8252/), "OAuth 2.0 for native +Applications". It provides types that describe OIDC requests and responses, low level +methods to construct protocol state and handle responses, and higher level methods for +logging in, logging out, retrieving userinfo, and refreshing tokens. + +- GitHub: +- NuGet: diff --git a/IdentityModel/outtakes/aspnetcore/_index.md b/FOSS/outtakes/aspnetcore/_index.md similarity index 100% rename from IdentityModel/outtakes/aspnetcore/_index.md rename to FOSS/outtakes/aspnetcore/_index.md diff --git a/IdentityModel/outtakes/aspnetcore/extensibility.md b/FOSS/outtakes/aspnetcore/extensibility.md similarity index 100% rename from IdentityModel/outtakes/aspnetcore/extensibility.md rename to FOSS/outtakes/aspnetcore/extensibility.md diff --git a/IdentityModel/outtakes/aspnetcore/images/Web.gif b/FOSS/outtakes/aspnetcore/images/Web.gif similarity index 100% rename from IdentityModel/outtakes/aspnetcore/images/Web.gif rename to FOSS/outtakes/aspnetcore/images/Web.gif diff --git a/IdentityModel/outtakes/aspnetcore/images/Worker.gif b/FOSS/outtakes/aspnetcore/images/Worker.gif similarity index 100% rename from IdentityModel/outtakes/aspnetcore/images/Worker.gif rename to FOSS/outtakes/aspnetcore/images/Worker.gif diff --git a/IdentityModel/outtakes/aspnetcore/overview.md b/FOSS/outtakes/aspnetcore/overview.md similarity index 100% rename from IdentityModel/outtakes/aspnetcore/overview.md rename to FOSS/outtakes/aspnetcore/overview.md diff --git a/IdentityModel/outtakes/aspnetcore/web.md b/FOSS/outtakes/aspnetcore/web.md similarity index 100% rename from IdentityModel/outtakes/aspnetcore/web.md rename to FOSS/outtakes/aspnetcore/web.md diff --git a/IdentityModel/outtakes/aspnetcore/worker.md b/FOSS/outtakes/aspnetcore/worker.md similarity index 100% rename from IdentityModel/outtakes/aspnetcore/worker.md rename to FOSS/outtakes/aspnetcore/worker.md diff --git a/IdentityModel/outtakes/client/end_session.md b/FOSS/outtakes/client/end_session.md similarity index 100% rename from IdentityModel/outtakes/client/end_session.md rename to FOSS/outtakes/client/end_session.md diff --git a/IdentityModel/outtakes/js/_index.md b/FOSS/outtakes/js/_index.md similarity index 100% rename from IdentityModel/outtakes/js/_index.md rename to FOSS/outtakes/js/_index.md diff --git a/IdentityModel/themes/hugo-theme-learn/.editorconfig b/FOSS/themes/hugo-theme-learn/.editorconfig similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/.editorconfig rename to FOSS/themes/hugo-theme-learn/.editorconfig diff --git a/IdentityModel/themes/hugo-theme-learn/.gitignore b/FOSS/themes/hugo-theme-learn/.gitignore similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/.gitignore rename to FOSS/themes/hugo-theme-learn/.gitignore diff --git a/IdentityModel/themes/hugo-theme-learn/.grenrc.yml b/FOSS/themes/hugo-theme-learn/.grenrc.yml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/.grenrc.yml rename to FOSS/themes/hugo-theme-learn/.grenrc.yml diff --git a/IdentityModel/themes/hugo-theme-learn/archetypes/chapter.md b/FOSS/themes/hugo-theme-learn/archetypes/chapter.md similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/archetypes/chapter.md rename to FOSS/themes/hugo-theme-learn/archetypes/chapter.md diff --git a/IdentityModel/themes/hugo-theme-learn/archetypes/default.md b/FOSS/themes/hugo-theme-learn/archetypes/default.md similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/archetypes/default.md rename to FOSS/themes/hugo-theme-learn/archetypes/default.md diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/ar.toml b/FOSS/themes/hugo-theme-learn/i18n/ar.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/ar.toml rename to FOSS/themes/hugo-theme-learn/i18n/ar.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/de.toml b/FOSS/themes/hugo-theme-learn/i18n/de.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/de.toml rename to FOSS/themes/hugo-theme-learn/i18n/de.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/en.toml b/FOSS/themes/hugo-theme-learn/i18n/en.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/en.toml rename to FOSS/themes/hugo-theme-learn/i18n/en.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/es.toml b/FOSS/themes/hugo-theme-learn/i18n/es.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/es.toml rename to FOSS/themes/hugo-theme-learn/i18n/es.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/fr.toml b/FOSS/themes/hugo-theme-learn/i18n/fr.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/fr.toml rename to FOSS/themes/hugo-theme-learn/i18n/fr.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/hi.toml b/FOSS/themes/hugo-theme-learn/i18n/hi.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/hi.toml rename to FOSS/themes/hugo-theme-learn/i18n/hi.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/id.toml b/FOSS/themes/hugo-theme-learn/i18n/id.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/id.toml rename to FOSS/themes/hugo-theme-learn/i18n/id.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/ja.toml b/FOSS/themes/hugo-theme-learn/i18n/ja.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/ja.toml rename to FOSS/themes/hugo-theme-learn/i18n/ja.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/nl.toml b/FOSS/themes/hugo-theme-learn/i18n/nl.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/nl.toml rename to FOSS/themes/hugo-theme-learn/i18n/nl.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/pt.toml b/FOSS/themes/hugo-theme-learn/i18n/pt.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/pt.toml rename to FOSS/themes/hugo-theme-learn/i18n/pt.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/ru.toml b/FOSS/themes/hugo-theme-learn/i18n/ru.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/ru.toml rename to FOSS/themes/hugo-theme-learn/i18n/ru.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/tr.toml b/FOSS/themes/hugo-theme-learn/i18n/tr.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/tr.toml rename to FOSS/themes/hugo-theme-learn/i18n/tr.toml diff --git a/IdentityModel/themes/hugo-theme-learn/i18n/zh-cn.toml b/FOSS/themes/hugo-theme-learn/i18n/zh-cn.toml similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/i18n/zh-cn.toml rename to FOSS/themes/hugo-theme-learn/i18n/zh-cn.toml diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/404.html b/FOSS/themes/hugo-theme-learn/layouts/404.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/404.html rename to FOSS/themes/hugo-theme-learn/layouts/404.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/_default/list.html b/FOSS/themes/hugo-theme-learn/layouts/_default/list.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/_default/list.html rename to FOSS/themes/hugo-theme-learn/layouts/_default/list.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/_default/single.html b/FOSS/themes/hugo-theme-learn/layouts/_default/single.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/_default/single.html rename to FOSS/themes/hugo-theme-learn/layouts/_default/single.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/index.html b/FOSS/themes/hugo-theme-learn/layouts/index.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/index.html rename to FOSS/themes/hugo-theme-learn/layouts/index.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/index.json b/FOSS/themes/hugo-theme-learn/layouts/index.json similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/index.json rename to FOSS/themes/hugo-theme-learn/layouts/index.json diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/custom-comments.html b/FOSS/themes/hugo-theme-learn/layouts/partials/custom-comments.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/custom-comments.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/custom-comments.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/custom-footer.html b/FOSS/themes/hugo-theme-learn/layouts/partials/custom-footer.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/custom-footer.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/custom-footer.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/custom-header.html b/FOSS/themes/hugo-theme-learn/layouts/partials/custom-header.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/custom-header.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/custom-header.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/favicon.html b/FOSS/themes/hugo-theme-learn/layouts/partials/favicon.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/favicon.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/favicon.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/footer.html b/FOSS/themes/hugo-theme-learn/layouts/partials/footer.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/footer.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/footer.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/header.html b/FOSS/themes/hugo-theme-learn/layouts/partials/header.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/header.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/header.html diff --git a/FOSS/themes/hugo-theme-learn/layouts/partials/logo.html b/FOSS/themes/hugo-theme-learn/layouts/partials/logo.html new file mode 100644 index 00000000..d6e6c3ba --- /dev/null +++ b/FOSS/themes/hugo-theme-learn/layouts/partials/logo.html @@ -0,0 +1,3 @@ + diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/menu-footer.html b/FOSS/themes/hugo-theme-learn/layouts/partials/menu-footer.html similarity index 100% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/menu-footer.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/menu-footer.html diff --git a/IdentityModel/themes/hugo-theme-learn/layouts/partials/menu.html b/FOSS/themes/hugo-theme-learn/layouts/partials/menu.html similarity index 97% rename from IdentityModel/themes/hugo-theme-learn/layouts/partials/menu.html rename to FOSS/themes/hugo-theme-learn/layouts/partials/menu.html index 5583eda0..bcd62015 100644 --- a/IdentityModel/themes/hugo-theme-learn/layouts/partials/menu.html +++ b/FOSS/themes/hugo-theme-learn/layouts/partials/menu.html @@ -39,11 +39,11 @@

{{ if not $disableShortcutsTitle}}{{ T "Shortcuts-Title"}}{{ end }}

{{end}} - {{ if or .Site.IsMultiLingual $showvisitedlinks }} + {{ if or hugo.IsMultilingual $showvisitedlinks }}