I have now managed to set up an IdentityServer app and a separate MVC client app where the IdentityServer uses the MVC app's user database. The MVC app is used for user registration, login, logout and calling additional APIs.

I can log in to the MVC app and also get a correct token back from the IdentityServer. But now I have two problems:

  1. The token in HttpContext is null, although IdentityServer issues a token sucessfully. But both following variants don't work when I try to access the token from HttpContext:
var accessToken = await HttpContext.GetTokenAsync("access_token");
var accessToken = await HttpContext.GetTokenAsync(IdentityConstants.ExternalScheme, "access_token");

Could the reason be that I start both projects with "dotnet run" and not with IIS in VS2017?

  1. Where or how do I save the token for the client for future calls of APIs? Ideally, the token is sent automatically with every request. Have I forgotten a configuration here?

This is how my configurations look like:

IdentityServer App:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    var builder = services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;
    })
        .AddInMemoryIdentityResources(Config.GetIdentityResources())
        .AddInMemoryApiResources(Config.GetApis())
        .AddInMemoryClients(Config.GetClients())
        .AddAspNetIdentity<ApplicationUser>();

    if (Environment.IsDevelopment())
    {
        builder.AddDeveloperSigningCredential();
    }
    else
    {
        throw new Exception("need to configure key material");
    }
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseStaticFiles();
    app.UseIdentityServer();
    app.UseMvcWithDefaultRoute();
}

Config.cs

public static IEnumerable<IdentityResource> GetIdentityResources()
{
    return new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
    };
}

public static IEnumerable<ApiResource> GetApis()
{
    return new List<ApiResource>
    {
        new ApiResource("api1", "My API")
    };
}

public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        new Client
        {
            ClientId = "mvc",
            ClientName = "MVC Client",
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },

            RedirectUris           = { "http://localhost:5002/signin-oidc" },
            PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                "api1"
            },

            AllowOfflineAccess = true
        }
    };
}

MVC Client App:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<IdentityUser>()
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
        .AddCookie("Cookies")
        .AddOpenIdConnect("oidc", options =>
        {
            options.SignInScheme = "Cookies";

            options.Authority = "http://localhost:5000";
            options.RequireHttpsMetadata = false;

            options.ClientId = "mvc";
            options.ClientSecret = "secret";
            options.ResponseType = "code id_token";

            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;

            options.Scope.Add("api1");
            options.Scope.Add("offline_access");

            options.ClaimActions.MapJsonKey("website", "website");
        });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseAuthentication();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseMvcWithDefaultRoute();
}

Test controller

public async Task<IActionResult> LoginTest()
{
    var client = new HttpClient();
    var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
    if (disco.IsError)
    {
        Console.WriteLine(disco.Error);
    }

    var tokenClient = new TokenClient(disco.TokenEndpoint, "mvc", "secret");
    var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("myusername", "mypassword");

    if (tokenResponse.IsError)
    {
        Console.WriteLine(tokenResponse.Error);
    }

    Console.WriteLine(tokenResponse.Json);
    Console.WriteLine("\n\n");


    //var accessToken = await HttpContext.GetTokenAsync("access_token");
    var accessToken = await HttpContext.GetTokenAsync(IdentityConstants.ExternalScheme, "access_token");

    return View();
}

tokenResponse is sucessfully populated:

info: IdentityServer4.Events.DefaultEventService[0]
{
"Name": "Token Issued Success",
"Category": "Token",
"EventType": "Success",
"Id": 2000,
"ClientId": "mvc",
"ClientName": "MVC Client",
"Endpoint": "Token",
"SubjectId": "08be5c35-5593-49f4-999b-e5ddd694f2e9",
"Scopes": "openid profile api1 offline_access",
"GrantType": "password",
"Tokens": [
  {
    "TokenType": "refresh_token",
    "TokenValue": "****55a0"
  },
  {
    "TokenType": "access_token",
    "TokenValue": "****YuFw"
  }
],
"ActivityId": "0HLMAGC28PO61:00000001",
"TimeStamp": "2019-04-26T20:09:27Z",
"ProcessId": 21208,
"LocalIpAddress": "::1:5000",
"RemoteIpAddress": "::1"
}

I am new with IdentityServer, so far I have developed a SPA with WebAPI and stored JWTs in the localstore. I think there is a more comfortable way to do this with an MVC client and IdentityServer.

1 Answers

0
d_f On Best Solutions

As you registered your OIDC middleware with

options.ResponseType = "code id_token";

you do not need any login controller in your client app. The middleware will redirect your user to Login page aka Authorize Endpoint of IdentityServer. Just add Authorize attribute to any secured controller and inside you should have:

string accessToken = await HttpContext.GetTokenAsync("access_token");
string idToken = await HttpContext.GetTokenAsync("id_token");

What you do in your example LoginTest() method is direct call to token endpoint, which does not employ OIDC middleware, and thus does not login your user (no user session created), but just validates the credentials you provide and creates tokens. To employ all the protocol features you additionally need to change

AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

to

AllowedGrantTypes = GrantTypes.Hybrid, //or .Code

and that's it