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

Update quickstarts for v7 #419

Merged
merged 1 commit into from
Jan 30, 2024
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
99 changes: 41 additions & 58 deletions IdentityServer/v7/docs/content/quickstarts/1_client_credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ the solution.

```console
cd ..
dotnet sln add ./src/IdentityServer/IdentityServer.csproj
dotnet sln add ./src/IdentityServer
```

### Defining an API Scope
Expand All @@ -113,14 +113,14 @@ complete access to an API that will be created later in this quickstart.

Scope definitions can be loaded in many ways. This quickstart shows how to use a
"code as configuration" approach. A minimal Config.cs was created by the
template at *src/IdentityServer/Config.cs*. Open it and add an *ApiScope* to the
template at *src/IdentityServer/Config.cs*. Open it and add a couple *ApiScope*s to the
*ApiScopes* property:

```csharp
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
new ApiScope[]
{
new ApiScope(name: "api1", displayName: "MyAPI")
new ApiScope(name: "api1", displayName: "My API")
};
```

Expand All @@ -147,7 +147,8 @@ Add this client definition to *Config.cs*:

```cs
public static IEnumerable<Client> Clients =>
new List<Client>
new Client

{
new Client
{
Expand Down Expand Up @@ -228,15 +229,15 @@ the .NET CLI to create the API project. To use the CLI, run the
following command from the *src* directory:

```console
dotnet new webapi -n Api
dotnet new webapi -n Api --no-openapi
```

Then navigate back up to the root quickstart directory and add it to the
solution by running the following commands:

```console
cd ..
dotnet sln add ./src/Api/Api.csproj
dotnet sln add ./src/Api
```

### Add JWT Bearer Authentication
Expand All @@ -253,24 +254,21 @@ middleware will
Run this command in the *src* directory to install the middleware
package in the Api:
```console
dotnet add ./Api/Api.csproj package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add ./Api package Microsoft.AspNetCore.Authentication.JwtBearer
```

Now add JWT Bearer authentication services to the Service Collection to allow
for dependency injection (DI), and configure *Bearer* as the default
[Authentication Scheme](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-6.0#authentication-scheme).
Now add the authentication and authorization services to the Service Collection, and
configure the JWT Bearer authentication provider as the default
[Authentication Scheme](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-8.0#authentication-scheme).

```csharp
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
builder.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001";

options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
options.TokenValidationParameters = false;
});
builder.Services.AddAuthorization();
```
{{% notice note %}}

Expand All @@ -282,35 +280,16 @@ more in-depth discussion.

{{% /notice %}}

Add authentication middleware to the pipeline immediately before authorization:
```csharp
app.UseAuthentication();
app.UseAuthorization();
```
*UseAuthentication* adds the authentication middleware to the pipeline so
authentication will be performed automatically on every call into the host.
*UseAuthorization* adds the authorization middleware to make sure your API
endpoint cannot be accessed by anonymous clients.

### Add a controller
Add a new class called *IdentityController* in *src/Api/Controllers*:
### Add an endpoint
Replace the templated weather forecast endpoint with a new endpoint:

```csharp
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
app.MapGet("identity", (ClaimsPrincipal user) => user.Claims.Select(c => new { c.Type, c.Value }))
.RequireAuthorization();
```
This controller will be used to test authorization and to display the claims
identity through the eyes of the API. See the full file
[here]({{< param qs_base >}}/1_ClientCredentials/src/Api/Controllers/IdentityController.cs).

This endpoint will be used to test authorization and to display the claims identity
through the eyes of the API.

### Configure API to listen on Port 6001

Expand All @@ -324,7 +303,7 @@ to be:
"applicationUrl": "https://localhost:6001"
```

### Test the controller
### Test the identity endpoint

Run the API project and then navigate to the identity controller at
*https://localhost:6001/identity* in a browser. This should return a 401 status
Expand All @@ -344,7 +323,7 @@ Then as before, add it to your solution using:

```console
cd ..
dotnet sln add ./src/Client/Client.csproj
dotnet sln add ./src/Client
```

### Add the IdentityModel nuget package
Expand All @@ -357,7 +336,7 @@ via Visual Studio's Nuget Package manager or dotnet CLI. From the *quickstart*
directory, run the following command:

```console
dotnet add ./src/Client/Client.csproj package IdentityModel
dotnet add ./src/Client package IdentityModel
```

### Retrieve the discovery document
Expand All @@ -367,6 +346,8 @@ endpoint addresses can be read from the metadata. Add the following to the
client's Program.cs in the *src/Client/Program.cs* directory:

```cs
using IdentityModel.Client;

// discover endpoints from metadata
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
Expand All @@ -379,10 +360,9 @@ if (disco.IsError)

{{% notice note %}}

If you get an error connecting it may be that you are running *https* and the
development certificate for *localhost* is not trusted. You can run `dotnet
dev-certs https --trust` in order to trust the development certificate. This
only needs to be done once.
If you get an error connecting, it may be that the development certificate for *localhost*
is not trusted. You can run `dotnet dev-certs https --trust` in order to trust the
development certificate. This only needs to be done once.

{{% /notice %}}

Expand All @@ -395,7 +375,6 @@ from *IdentityServer* to access *api1*:
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,

ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
Expand All @@ -404,6 +383,7 @@ var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCr
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
Console.WriteLine(tokenResponse.ErrorDescription);
return;
}

Expand All @@ -424,7 +404,7 @@ header. This is done using the *SetBearerToken* extension method:
```cs
// call api
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
apiClient.SetBearerToken(tokenResponse.AccessToken!); // AccessToken is always non-null when IsError is false

var response = await apiClient.GetAsync("https://localhost:6001/identity");
if (!response.IsSuccessStatusCode)
Expand Down Expand Up @@ -469,7 +449,7 @@ scope, lifetime (nbf and exp), the client ID (client_id) and the issuer name
#### Authorization at the API
Right now, the API accepts any access token issued by your IdentityServer. In
this section, you will add an [Authorization
Policy](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-6.0)
Policy](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-8.0)
to the API that will check for the presence of the "api1" scope in the access
token. The protocol ensures that this scope will only be in the token if the
client requests it and IdentityServer allows the client to have that scope. You
Expand All @@ -491,16 +471,19 @@ builder.Services.AddAuthorization(options =>
You can now enforce this policy at various levels, e.g.:

* globally
* for all API endpoints
* for specific controllers/actions
* for all endpoints
* for specific controllers, actions, or endpoints

Typically you set the policy for all controllers where they are mapped in
*src/Api/Program.cs*:
Add the policy to the identity endpoint in *src/Api/Program.cs*:

```cs
app.MapControllers().RequireAuthorization("ApiScope");
app.MapGet("identity", (ClaimsPrincipal user) => user.Claims.Select(c => new { c.Type, c.Value }))
.RequireAuthorization("ApiScope");
```

Now you can run the API again and it will enforce that the api1 scope is present in the
access token.

## Further experiments
This quickstart focused on the success path:

Expand Down
42 changes: 20 additions & 22 deletions IdentityServer/v7/docs/content/quickstarts/2_interactive.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ last name, etc) scopes by declaring them in *src/IdentityServer/Config.cs*:

```cs
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
Expand Down Expand Up @@ -158,7 +158,7 @@ public static IEnumerable<Client> Clients =>
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },

AllowedScopes = new List<string>
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
Expand All @@ -175,13 +175,13 @@ the following commands from the *src* directory:
```console
dotnet new webapp -n WebClient
cd ..
dotnet sln add ./src/WebClient/WebClient.csproj
dotnet sln add ./src/WebClient
```

{{% notice note %}}

This version of the quickstarts uses [Razor
Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-6.0&tabs=visual-studio)
Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-8.0&tabs=visual-studio)
for the web client. If you prefer MVC, the conversion is straightforward. See
the [quickstart for IdentityServer
5](https://docs.duendesoftware.com/identityserver/v5/quickstarts/2_interactive/)
Expand All @@ -199,15 +199,9 @@ dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
```

### Configure Authentication Services
Then add the following to *ConfigureServices* in *src/WebClient/Program.cs*:
Then add the authentication service and register the cookie and OpenIdConnect authentication providers in *src/WebClient/Program.cs*:

```cs
using System.IdentityModel.Tokens.Jwt;

// ...

JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
Expand All @@ -226,6 +220,8 @@ builder.Services.AddAuthentication(options =>
options.Scope.Add("openid");
options.Scope.Add("profile");

options.MapInboundClaims = false; // Don't rename claim types

options.SaveTokens = true;
});
```
Expand Down Expand Up @@ -285,7 +281,7 @@ app.MapRazorPages().RequireAuthorization();
{{% notice note %}}

See the ASP.NET Core documentation on [Razor Pages authorization
conventions](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/razor-pages-authorization?view=aspnetcore-6.0)
conventions](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/razor-pages-authorization?view=aspnetcore-8.0)
for more options that allow you to specify authorization on a per page or
directory basis.

Expand Down Expand Up @@ -315,7 +311,7 @@ the cookie properties:
<h2>Properties</h2>

<dl>
@foreach (var prop in (await HttpContext.AuthenticateAsync()).Properties.Items)
@foreach (var prop in (await HttpContext.AuthenticateAsync()).Properties!.Items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
Expand All @@ -329,7 +325,8 @@ Update the client's applicationUrl in

```json
{
"profiles": {
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"WebClient": {
"commandName": "Project",
"dotnetRunMessages": true,
Expand Down Expand Up @@ -417,10 +414,12 @@ within the navbar-nav list:
</ul>
```

Run the application again, and try logging out. Observe that you get redirected to the end session endpoint, and that both session cookies are cleared.

## Getting claims from the UserInfo endpoint
You might have noticed that even though you've configured the client to be
allowed to retrieve the *profile* identity scope, the claims associated with
that scope (such as *name*, *family_name*, *website* etc.) don't appear in the
that scope (such as *name*, *given_name*, *family_name*, etc.) don't appear in the
returned token. You need to tell the client to retrieve those claims from the
userinfo endpoint by specifying scopes that the client application needs to
access and setting the *GetClaimsFromUserInfoEndpoint* option. Add the following
Expand All @@ -438,9 +437,8 @@ to *ConfigureServices* in *src/WebClient/Program.cs*:
});
```

After restarting the client app, logging out, and logging back in, you should
see additional user claims associated with the *profile* identity scope
displayed on the page.
After restarting the client app and logging back in, you should see additional user claims
associated with the *profile* identity scope displayed on the page.

![](../images/2_additional_claims.png)

Expand Down Expand Up @@ -485,7 +483,7 @@ To add more claims to the identity:
{
ClientId = "web",
//...
AllowedScopes = new List<string>
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
Expand All @@ -495,7 +493,7 @@ To add more claims to the identity:
```
* Request the resource by adding it to the *Scopes* collection on the OpenID
Connect handler configuration in *src/WebClient/Program.cs*, and add a
[ClaimAction](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.claimactions?view=aspnetcore-6.0)
[ClaimAction](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.claimactions?view=aspnetcore-8.0)
to map the new claim returned from the userinfo endpoint onto a user claim.
```csharp
.AddOpenIdConnect("oidc", options =>
Expand Down Expand Up @@ -533,7 +531,7 @@ To use Google for authentication, you need to:
it.

See [Microsoft's
guide](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-6.0#create-a-google-api-console-project-and-client-id)
guide](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-8.0#create-a-google-api-console-project-and-client-id)
for details on how to register with Google, create the client, and store the
secrets in user-secrets. **Stop before adding the authentication middleware and
Google authentication handler to the pipeline.** You will need an
Expand All @@ -554,7 +552,7 @@ builder.Services.AddAuthentication()
```

When authenticating with Google, there are again two [authentication
schemes](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-6.0#authentication-scheme).
schemes](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-8.0#authentication-scheme).
*AddGoogle* adds the Google scheme, which handles the protocol flow back and
forth with Google. After successful login, the application needs to sign in to
an additional scheme that can authenticate future requests without needing a
Expand Down
Loading
Loading