dotnet / dotnet/aspnetcore

SigninManager.ExternalLoginSignInAsync(...) Does Not Store Authentication-Tokens

Open
#12,047 4 comments 1 reaction 0 assignees View on GitHub
affected-most area-identity enhancement severity-minor
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Description
I use **ASP.NET Core 2 Identity** with **OpenIdConnect** on **IdentityServer3**. After my external authentication callback is called, I use `signInManager.ExternalLoginSignInAsync(...)` to **sign in - and it works**.

What does *not work* is, that in the **next request** any call to `HttpContext.GetTokenAsync(...)` **fails** for any possible authentication-scheme (token "id_token"). This is bad because for OpenIdConnectHandler not being able to retrieve the `id_token` from the current context, it doesn't provide the `id_token` to the logout endpoint of IdentityServer. IdentityServer logs the user out but doesn't accept the `post_logout_redirect_uri` because of this.

But I have found a *workaround*.

### To Reproduce
1. Create a new **ASP.NET Core 2.2** application with **ASP.NET Core 2 Identity** support
2. Configure **OpenIdConnect**, like

```csharp
services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
options.DefaultSignOutScheme = IdentityConstants.ApplicationScheme;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://devnavis.premedia.at/SecurityTokenService";
options.ClientId = "";
options.ClientSecret = "";
options.RequireHttpsMetadata = false;

// callback path is provided by middleware
options.CallbackPath = "/openidconnect-signin-callback";
options.SaveTokens = true;

options.SignInScheme = IdentityConstants.ExternalScheme;
options.SignOutScheme = IdentityConstants.ApplicationScheme;
options.SignedOutRedirectUri = "/account/logged-out";
options.SignedOutCallbackPath = "/openidconnect-signout-callback";
// configure for returning id_token
options.ResponseType = OpenIdConnectResponseType.IdTokenToken;
options.GetClaimsFromUserInfoEndpoint = true;

options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");

options.Events = new OpenIdConnectEvents
{
OnTicketReceived = async e =>
{
// slim down claims if claims are too much
e.Principal = TransformClaims(e.Principal);
}
};
})
...
private static ClaimsPrincipal TransformClaims(ClaimsPrincipal claimsPrincipal)
{
var identity = (ClaimsIdentity)claimsPrincipal.Identity;
var email = identity.FindFirst("email");
var name = identity.FindFirst("name");
var sub = identity.FindFirst("sub");
var nameidentifier = identity.FindFirst(ClaimTypes.NameIdentifier);
var claimsToKeep = new[]
{
email,
name,
sub,
nameidentifier
};
var newIdentity = new ClaimsIdentity(claimsToKeep, identity.AuthenticationType, "name", "role");
var newClaims = new ClaimsPrincipal(newIdentity);
return newClaims;
}
```

3. In a login contoller method, call `signInManager.ConfigureExternalAuthenticationProperties(...)` with OpenIdConnect scheme and proper redirect url, like

```csharp
var properties = signInManager.ConfigureExternalAuthenticationProperties(OpenIdConnectDefaults.AuthenticationScheme, $"/account/logged-in");
await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties).ConfigureAwait(false);
```

4. When the authentication comes back (logged-in), call

```csharp
var externalInfo = await signInManager.GetExternalLoginInfoAsync();
var signInResult = await signInManager.ExternalLoginSignInAsync(externalInfo.LoginProvider, externalInfo.ProviderKey, true, true).ConfigureAwait(false);
if (signInResult.Succeeded)
return Redirect(redirect_url);
else if (signInResult.IsLockedOut)
return Unauthorized();
else // new user
{
var email = externalInfo.Principal.FindFirstValue(ClaimTypes.Email) ??
externalInfo.Principal.FindFirstValue("email");
var name = externalInfo.Principal.FindFirstValue(ClaimTypes.Name) ??
externalInfo.Principal.FindFirstValue("name");
var user = new User
{
Email = email,
UserName = name,
Name = name
};
var resultCreateUser = await userManager.CreateAsync(user);
if (resultCreateUser.Succeeded)
{
resultCreateUser = await userManager.AddLoginAsync(user, externalInfo);
if (resultCreateUser.Succeeded)
{
logger.LogInformation("User created an account using {Name} provider.", externalInfo.LoginProvider);
return Redirect(redirect_url);
}
}
}
return Unauthorized();
```

5. The user is *successfully logged* in
6. Navigate to another Controller-Action like `/account/token` which has something, like

```csharp
var idToken = await HttpContext.GetTokenAsync("id_token")
```
7. `idToken` is **null**

### Workaround
Supplement the code from step 4 with *another* `SignIn(...)` call with **custom `AuthenticationProperties`**, which copy the authentication tokens to the authentication cookie (as far as I understand).

```csharp
var externalInfo = await signInManager.GetExternalLoginInfoAsync();
var signInResult = await signInManager.ExternalLoginSignInAsync(externalInfo.LoginProvider, externalInfo.ProviderKey, true, true).ConfigureAwait(false);
if (signInResult.Succeeded)
{
// workaround
var props = new AuthenticationProperties();
props.StoreTokens(externalInfo.AuthenticationTokens);
props.IsPersistent = true;
var user = await userManager.FindByLoginAsync(externalInfo.LoginProvider, externalInfo.ProviderKey);
// provide custom AuthenticationProperties so tokens get persisted!
await signInManager.SignInAsync(user, props, externalInfo.LoginProvider);**
// workaround end

return Redirect(redirect_url);
}
else if (signInResult.IsLockedOut)
return Unauthorized();
else // new user
{
var email = externalInfo.Principal.FindFirstValue(ClaimTypes.Email) ??
externalInfo.Principal.FindFirstValue("email");
var name = externalInfo.Principal.FindFirstValue(ClaimTypes.Name) ??
externalInfo.Principal.FindFirstValue("name");
var user = new User
{
Email = email,
UserName = name,
Name = name
};
var resultCreateUser = await userManager.CreateAsync(user);
if (resultCreateUser.Succeeded)
{
resultCreateUser = await userManager.AddLoginAsync(user, externalInfo);
if (resultCreateUser.Succeeded)
{
// workaround
var props = new AuthenticationProperties();
props.StoreTokens(externalInfo.AuthenticationTokens);
props.IsPersistent = true;
var user = await userManager.FindByLoginAsync(externalInfo.LoginProvider, externalInfo.ProviderKey);
// provide custom AuthenticationProperties so tokens get persisted!
await signInManager.SignInAsync(user, props, externalInfo.LoginProvider);**
// workaround end

logger.LogInformation("User created an account using {Name} provider.", externalInfo.LoginProvider);
return Redirect(redirect_url);
}
}
}
return Unauthorized();
```

### Expected behavior
`HttpContext.GetTokenAsync("id_token")` should return non- null id-token

### Suggestion
Either provide a way to pass in custom `AuthenticationProperties` to `signInManager.ExternalLoginSignInAsync(...)`, or enable copying over the `AuthenticationTokens` from `ExternalLoginInfo`.

### Additional context
netcoreapp2.2

#### Relevant Nugets
```xml

```
#### dotnet --info
```
.NET Core SDK (gemäß "global.json"):
Version: 2.2.300
Commit: 73efd5bd87

Laufzeitumgebung:
OS Name: Windows
OS Version: 10.0.17134
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\2.2.300\

Host (useful for support):
Version: 2.2.5
Commit: 0a3c9209c0

.NET Core SDKs installed:
2.0.3 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.502 [C:\Program Files\dotnet\sdk]
2.1.503 [C:\Program Files\dotnet\sdk]
2.1.504 [C:\Program Files\dotnet\sdk]
2.1.507 [C:\Program Files\dotnet\sdk]
2.1.602 [C:\Program Files\dotnet\sdk]
2.1.604 [C:\Program Files\dotnet\sdk]
2.1.700 [C:\Program Files\dotnet\sdk]
2.2.104 [C:\Program Files\dotnet\sdk]
2.2.107 [C:\Program Files\dotnet\sdk]
2.2.204 [C:\Program Files\dotnet\sdk]
2.2.300 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
Microsoft.AspNetCore.All 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]

To install additional .NET Core runtimes or SDKs:
https://aka.ms/dotnet-download
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.