Delegation or Impersonation
There are situations in which a REST API needs to act on behalf of users or other clients.
A subject can act on behalf of another subject in two ways: either through impersonation or delegation.
Impersonation occurs when the subject in a token is indistinguishable from the original one. The recipient of the token will typically be unaware that impersonation is taking place.
In case of a Delegation , a token contains explicit information that one subject delegates its rights to another entity. The call chain is preserved using the act claim.
In this tutorial, we will implement the following delegation scenario. The SecondAPI REST API acts on behalf of the end-user.
- The website calls a REST API to retrieve the list of shops.
- The REST API uses the
urn:ietf:params:oauth:grant-type:token-exchangegrant type to receive an access token valid on theshopApiOtherscope and acts on behalf of the authenticated user. This access token is used to access the second REST API. - The REST API receives the access token and checks its audience.
1. Define the first permission
- Open the Identity Server website at https://localhost:5002/master/clients.
- Open the Scopes screen and click on the
Add scopebutton. - Select
API valueand click on next. - Fill-in the form like this and click on the
Savebutton to confirm the creation.
| Parameter | Value |
|---|---|
| Name | shopApi |
| Description | shopApi |
- Navigate to the new scope, then select the
API Resourcestab and click on theAdd API resourcebutton. - Fill-in the form like this and click on the
Addbutton to confirm the creation.
| Parameter | Value |
|---|---|
| Name | shopApi |
| Audience | shopApi |
| Description | shopApi |
2. Define the second permission
- Open the Identity Server website at https://localhost:5002/master/clients.
- Open the Scopes screen, click on the
Add scopebutton. - Select
API valueand click on next. - Fill-in the form like this and click on the
Savebutton to confirm the creation.
| Parameter | Value |
|---|---|
| Name | shopApiOther |
| Description | shopApiOther |
- Navigate to the new scope, then select the
API Resourcestab and click on theAdd API resourcebutton. - Fill-in the form like this and click on the
Addbutton to confirm the creation.
| Parameter | Value |
|---|---|
| Name | shopApiOther |
| Audience | shopApiOther |
| Description | shopApiOther |
3. Configure the website
- Open the Identity Server website at https://localhost:5002/master/clients.
- On the Client screen, click on the
Add client button. - Select
web applicationand click on next. - Fill-in the form like this and click on the
Savebutton to confirm the creation.
| Parameter | Value |
|---|---|
| Identifier | delegationWebsite |
| Secret | password |
| Name | delegationWebsite |
| Redirection URLS | http://localhost:5004/signin-oidc |
- Click on the new client, then select the
Client scopestab and click onAdd client scopebutton. Choose theshopApiscope and click on theSavebutton.
4. Configure the API
- Open the Identity Server website at https://localhost:5002/master/clients.
- On the Client screen, click on the
Add client button. - Select
machineand click on next. - Fill-in the form like this and click on the
Savebutton to confirm the creation.
| Parameter | Value |
|---|---|
| Identifier | firstDelegationApi |
| Secret | password |
| Name | firstDelegationApi |
- Click on the new client, in the
Detailstab, select theToken exchangegrant-type, set the token exchange type toDelegationand click on theSavebutton. - Select the
Client scopestab and click onAdd client scopebutton. Choose theshopApiOtherscope and click on theSavebutton.
5. Create the first API
Create and configure the first REST.API service
- Open a command prompt and execute the following commands to create the directory structure for the solution.
mkdir Delegation
cd Delegation
mkdir src
dotnet new sln -n Delegation
- Create a REST.API project named
ShopApiOtherand install theMicrosoft.AspNetCore.Authentication.JwtBearerNuget package.
cd src
dotnet new webapi -n ShopApiOther
cd ShopApiOther
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
- Add the
ShopApiOtherproject into your Visual Studio solution.
cd ..\..
dotnet sln add ./src/ShopApiOther/ShopApiOther.csproj
- Edit the file
ShopApiOther\Program.csand configure the JWT authentication. Additionally, add an Authorization policy namedShopsthat checks if the access token has the correct audience.
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001/master";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new List<string>
{
"shopApiOther"
},
ValidIssuers = new List<string>
{
"https://localhost:5001/master"
}
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Shops", p => p.RequireAuthenticatedUser());
});
- Add a
ShopsControllercontroller with one protected operation.
[ApiController]
[Route("shops")]
public class ShopsController : ControllerBase
{
[HttpGet]
[Authorize("Shops")]
public IActionResult Get()
{
return new OkObjectResult(new[] { "shop1", "shop2" });
}
}
- In a command prompt, navigate to the
src\ShopApiOtherdirectory and launch the application.
dotnet run --urls=http://localhost:5006
6. Create the second API
Create and configure the second REST.API service
- Create a REST.API project named
ShopApiand install theMicrosoft.AspNetCore.Authentication.JwtBearerNuget package.
cd src
dotnet new webapi -n ShopApi
cd ShopApi
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
- Add the
ShopApiproject into your Visual Studio solution.
cd ..\..
dotnet sln add ./src/ShopApi/ShopApi.csproj
- Edit the file
ShopApi\Program.csand configure the JWT authentication. Additionally, add an Authorization policy namedShopsthat checks if the access token has the correct audience.
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001/master";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new List<string>
{
"shopApi"
},
ValidIssuers = new List<string>
{
"https://localhost:5001/master"
}
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Shops", p => p.RequireAuthenticatedUser());
});
- Add a
ShopsControllercontroller with one protected operation. It uses theurn:ietf:params:oauth:grant-type:token-exchangegrant type to obtain an access token valid for theshopApiOtherscope and utilizes it to make a call to the first REST API service.
[ApiController]
[Route("shops")]
public class ShopsController : ControllerBase
{
[HttpGet]
[Authorize("Shops")]
public async Task<IActionResult> Get()
{
const string url = "http://localhost:5006/shops";
var accessToken = await GetAccessToken();
using (var httpClient = new HttpClient())
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}");
var httpResult = await httpClient.SendAsync(requestMessage);
var json = await httpResult.Content.ReadAsStringAsync();
var shopNames = JsonSerializer.Deserialize<string[]>(json);
return new OkObjectResult(shopNames);
}
}
private async Task<string> GetAccessToken()
{
const string clientId = "firstDelegationAPi";
const string clientSecret = "password";
const string tokenUrl = "https://localhost:5001/master/token";
var subjectToken = Request.Headers["Authorization"].ElementAt(0).Split(" ")[1];
using (var httpClient = new HttpClient())
{
var dic = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("subject_token", subjectToken),
new KeyValuePair<string, string>("subject_token_type", "urn:ietf:params:oauth:token-type:access_token"),
new KeyValuePair<string, string>("scope", "shopApiOther")
};
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
Content = new FormUrlEncodedContent(dic),
RequestUri = new Uri(tokenUrl)
};
var httpResult = await httpClient.SendAsync(requestMessage);
var json = await httpResult.Content.ReadAsStringAsync();
var accessToken = (JsonObject.Parse(json) as JsonObject).First(k => k.Key == "access_token").Value.ToString();
return accessToken;
}
}
}
The access contains the following claims :
| Claim | Description | Value |
|---|---|---|
| sub | subject of the authenticated user | administrator |
| act | Contains the chain of delegation | {act : sub : shopApi } |
| scope | List of scopes | shopApiOther |
| aud | Audience | shopApiOther |
| client_id | Client identifier | delegationWebsite |
- In a command prompt, navigate to the
src\ShopApidirectory and launch the application.
dotnet run --urls=http://localhost:5005
7. Create the website
Create and configure an ASP.NET CORE application
- Create a web project named
Websiteand install the Microsoft.AspNetCore.Authentication.OpenIdConnect NuGet package.
cd src
dotnet new mvc -n Website
cd Website
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
- Add the
Websiteproject into your Visual Studio solution.
cd ..\..
dotnet sln add ./src/Website/Website.csproj
- Edit the
Program.csfile and configure the OpenID authentication.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "sid";
})
.AddCookie("Cookies")
.AddOpenIdConnect("sid", options =>
{
options.SignInScheme = "Cookies";
options.ResponseType = "code";
options.Authority = "https://localhost:5001/master";
options.RequireHttpsMetadata = false;
options.ClientId = "delegationWebsite";
options.ClientSecret = "password";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Scope.Add("shopApi");
});
...
app.UseCookiePolicy(new CookiePolicyOptions
{
Secure = CookieSecurePolicy.Always
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
- Add a
ShopsControllercontroller with one protected operation.
public class ShopsController : Controller
{
[Authorize]
public async Task<IActionResult> Index()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
const string url = "http://localhost:5005/shops";
using(var httpClient = new HttpClient())
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}");
var httpResult = await httpClient.SendAsync(requestMessage);
var json = await httpResult.Content.ReadAsStringAsync();
var shopNames = JsonSerializer.Deserialize<string[]>(json);
return View(new ShopsViewModel { Shops = shopNames });
}
}
}
- Create a view
Views\Shops\Index.cshtmlwith the following content. This view will display the names of the Shop. - In a command prompt, navigate to the
src\Websitedirectory and launch the application.
dotnet run --urls=http://localhost:5004
Finally, browse the following URL: http://localhost:5004/shops. The User-Agent will be automatically redirected to the OpenID server. Submit the following credentials and confirm the consent. You will be redirected to the screen where the shops will be displayed.
| Credential | Value |
|---|---|
| Login | administrator |
| Password | password |