Zzz
Zzz

Reputation: 3025

How to us openid connect hybrid flow to call an Api on behalf of user (IdentityServer4 Asp.Net Core 2.0)?

I am attempting to use IdentityServer4 to have a web application call an API on behalf of the user. I expect that the API will have the User Identity information for the user even though it is the web application making the request.

Everything is working correctly (Authentication/Claims etc) except within the API User.Identity.Name is null. User.Identity.Name in the Web application is returning the correct username. bellow is an example of what I have tried.

I am using IdentityServer4 v- 2.1.2 IdentityModel v- 3.1.1 Microsoft.AspNetCore.All v- 2.0.5

Just as some backround: I am folowing a PluralSight Tutorial - https://app.pluralsight.com/library/courses/aspnet-core-identity-management-playbook/table-of-contents

API Controller

[Produces("application/json")]
[Route("api")]
public class ApiController : Controller
{
    [Route("user")]
    [Authorize]
    public IActionResult GetUser()
    {
        return Content("User " + User.Identity.Name);
    }
}

API Startup.cs - ConfigureServices method

services.AddAuthentication()
    .AddJwtBearer(options => 
    {
        options.Authority = "https://localhost:44335";
        options.Audience = "DemoApi";
        options.TokenValidationParameters.NameClaimType = "name";
    });

services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .Build();
});

IdentityServer InMemory Client setup

new Client()
{
    ClientId = "WebApp",
    AllowedGrantTypes = GrantTypes.Hybrid,
    ClientSecrets = new [] {new Secret("MySecret".Sha256())},
    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "DemoApi"
    },
    RedirectUris = { "https://localhost:44343/signin-oidc" },
    PostLogoutRedirectUris = new List<string>
    {
        "https://localhost:44343/signout-callback-oidc"
    },
    AllowOfflineAccess = true,
    RequireConsent = false
}

IdentityServer TestUser setup:

new TestUser()
{
    SubjectId = "1",
    Username = "testname",
    Password = "pass123",
    Claims = new []
    {
        new Claim("name", "testname")
    }
}

Web Application startup.cs ConfigureServices Method - Authentication setup

services.AddAuthentication(options => {
    options.DefaultChallengeScheme = "oidc";
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = "oidc";
}).AddOpenIdConnect("oidc", options => {
    options.Authority = "https://localhost:44335/";
    options.ClientId = "WebApp";
    options.ClientSecret = "MySecret";
    options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    options.Scope.Add("DemoApi");
    options.Scope.Add("offline_access");
    options.SignedOutRedirectUri = "/";
    options.TokenValidationParameters.NameClaimType = "name";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;
}).AddCookie();

Web Application API call -

[Route("callapi")]
[Authorize]
public async Task<IActionResult> CallApi()
{
    string accessToken;
    try
    {
        accessToken = await GetAccessToken();
    }
    catch (System.Exception ex)
    {
        ViewBag.Error = ex.GetBaseException().Message;
        return View();
    }

    var client = new HttpClient();
    client.SetBearerToken(accessToken);
    try
    {
        var content = await client.GetStringAsync("https://localhost:44379/api/user");
        ViewBag.ApiResponse = content;
    }
    catch (Exception ex)
    {
        ViewBag.ApiResponse = ex.GetBaseException().Message;                
    }

    ViewBag.AccessToken = accessToken;
    ViewBag.RefreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

    return View();
}

private async Task<string> GetAccessToken()
{
    var exp = await HttpContext.GetTokenAsync("expires_at");
    var expires = DateTime.Parse(exp);

    if (expires > DateTime.Now)
    {
        return await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
    }

    return await GetRefreshedAccessToken();
}

private async Task<string> GetRefreshedAccessToken()
{
    var disco = await DiscoveryClient.GetAsync("https://localhost:44335/");
    var tokenClient = new TokenClient(disco.TokenEndpoint, "WebApp", "MySecret");
    var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
    var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken);

    if (tokenResponse.IsError)
    {
        var auth = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        auth.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken);
        auth.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken);
        var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
        auth.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, auth.Principal, auth.Properties);
        return tokenResponse.AccessToken;
    }

    throw tokenResponse.Exception;
}

API - User.Identity enter image description here

Upvotes: 0

Views: 1927

Answers (1)

m3n7alsnak3
m3n7alsnak3

Reputation: 3156

Try setting up your API according to the official documentation.

From what I see, the differences are in the authentication type. You have

services.AddAuthentication()
            .AddJwtBearer(options => 
            {
                options.Authority = "https://localhost:44335";
                options.Audience = "DemoApi";
                options.TokenValidationParameters.NameClaimType = "name";
            });

while the docs say:

services.AddAuthentication("Bearer")
        .AddIdentityServerAuthentication(options =>
        {
            options.Authority = "http://localhost:5000";
            options.RequireHttpsMetadata = false;

            options.ApiName = "api1";
        });

Of course modify it with your authority url. The other things seem legit.

PS: The AddIdentityServerAuthentication is coming with IdentityServer4.AccessTokenValidation package

EDIT

Based on comments - remove this lines:

services.AddAuthorization(options =>
        {
            options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
                .RequireAuthenticatedUser()
                .Build();
        });

And lets see what happens.

EDIT 2

After some discussions, we figured out that Zzz was using some Pluralsight tutorial for asp.net identity. For those who are reading this, and they are starting implementing IdentityServer as an authentication for their applications - follow the official documentation and also check the samples.

PS: And always enable logging. It is everything and saves tons of hours.

Upvotes: 2

Related Questions