I have the following set up: Authorization server (.NET 6 with MVC, port 7000), Client (.NET 6 with MVC, port 7001), Resource Server (.NET 6 API, port 7002).
Authorization server set up:
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["ClientId"];
options.ClientSecret = builder.Configuration["ClientSecret"];
});
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
options.ClaimsIdentity.EmailClaimType = Claims.Email;
options.SignIn.RequireConfirmedAccount = false;
});
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<AuthorizationContext>();
})
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetIntrospectionEndpointUris("/connect/introspect");
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
options.AllowAuthorizationCodeFlow();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
builder.Services.AddHostedService<Worker>();
The seeded clients:
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
PostLogoutRedirectUris =
{
new Uri("https://localhost:7001/signout-callback-oidc")
},
RedirectUris =
{
new Uri("https://localhost:7001/signin-oidc")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "api1"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
// resource server
if (await manager.FindByClientIdAsync("resource_server_1") == null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "resource_server_1",
ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342",
Permissions =
{
Permissions.Endpoints.Introspection
}
};
await manager.CreateAsync(descriptor);
}
Client config:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromMinutes(50);
options.SlidingExpiration = false;
})
.AddOpenIdConnect(options =>
{
options.ClientId = "mvc";
options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654";
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Authority = "https://localhost:7000/";
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("api1");
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
});
Resource Server config:
builder.Services.AddOpenIddict()
.AddValidation(options =>
{
options.SetIssuer("https://localhost:7000/");
options.AddAudiences("resource_server_1");
options.UseIntrospection()
.SetClientId("resource_server_1")
.SetClientSecret("846B62D0-DEF9-4215-A99D-86E6B8DAB342");
options.UseSystemNetHttp();
options.UseAspNetCore();
});
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
This is how the client makes request to resource server:
[Authorize, HttpPost("~/")]
public async Task<ActionResult> Index(CancellationToken cancellationToken)
{
var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectParameterNames.AccessToken);
if (string.IsNullOrEmpty(token))
{
throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " +
"Make sure that SaveTokens is set to true in the OIDC options.");
}
using var client = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:7002/api/message");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using var response = await client.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
return View("Home", model: await response.Content.ReadAsStringAsync());
}
The problem is following when I set up those 3 instances (auth server, client, resource server) and I am NOT authenticated in the client (no cookies). I can authenticate on the client (and therefore on auth server). Then I make the request from the client to the resource server and it returns 200
.
But then I stop all 3 instances and try to do it again.
At that time I'm already authenticated in the client (cookies) and can extract token (FYI the tokens are the same between requests before stopping instances and after). But this token is invalid and the response code from the resource server is 401
.
On the resource server logs I can see the following logs: "OpenIddict.Validation.AspNetCore was not authenticated. Failure message: An error occurred while authenticating the current request
", and "invalid_token, the specified token is invalid
"
The question: is it expected behavior? I assume the reason is that data protection changed key ring or something like that. If it is expected - then how to do redeploys without reauthenticating all the users?