I am building an ASP.NET Core MVC web application that has two ways of authenticating; one way uses .AddOpenIdConnect()
and is already working.
The second way is to retrieve OAuth tokens from a web service.
// MVC
public async Task<IActionResult> GetToken()
{
var tokenResponse = await _tokenService.GetToken();
// This header did not appear in the JwtBearerEvents
string headerValue = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken).ToString();
Request.Headers.Authorization = headerValue;
return RedirectToAction("ShowToken", "Account");
}
// This AuthorizeAttribute causes JwtBearerEvents to fire
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult ShowToken()
{
return View();
}
After retrieving tokens from a web service,
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenHandlers.Add(new StrictTokenHandler());
options.TokenValidationParameters = tokenValidationParameters;
options.SaveToken = true;
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var p = context.Properties;
// context.Token = <context does not contain token>
return Task.CompletedTask;
},
OnChallenge = context =>
{
var p = context.Properties;
// have token, but can't set Token
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
},
};
})
.AddCookie(options =>
{
options.LoginPath = new PathString("/Account/Login");
})
.AddOpenIdConnect(options =>
{
options.Authority = issuer;
// the rest omitted for brevity
// this is working
});
In the .AddJwtBearer()
method above, the context MessageReceivedContext
has a settable Token
property, but I don't have it yet. The OnChallenge
event has the token if I set it in AuthenticationProperties
, but JwtBearerChallengeContext
does not have a settable Token
property.
This is the token information returned from the token service. How do I get this into the Authentication middleware?
Original Answer
I have found a workaround of sorts, however I am not yet convinced that it is the best solution. (Update: I found a better solution, see update below.)
The key seems to be to set the authorization header like this:
string accessToken = tokenResponse.AccessToken;
Request.Headers.Authorization = $"Bearer {accessToken}";
...right before calling AuthenticateAsync()
like this:
var result = await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
// MVC
public async Task<IActionResult> GetToken()
{
var tokenResponse = await _tokenService.GetToken();
// just for ShowToken - not required
TempData.Serialize("tokenInfo", new TokenViewModel()
{
TokenType = tokenResponse.TokenType,
Expires = tokenResponse.Expires,
AccessToken = tokenResponse.AccessToken,
Scope = tokenResponse.Scope,
RefreshToken = tokenResponse.RefreshToken,
IdToken = tokenResponse.IdToken,
});
// this seems like a hack, but works - is there a better way?
string accessToken = tokenResponse.AccessToken;
Request.Headers.Authorization = $"Bearer {accessToken}";
var result = await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
if (result.Principal != null)
{
await HttpContext.SignInAsync(result.Principal);
return RedirectToAction(nameof(AccountController.ShowToken), "Account");
}
else
{
return Challenge(
new AuthenticationProperties { RedirectUri = Url.Action("Index", "Home") },
OpenIdConnectDefaults.AuthenticationScheme);
}
}
[HttpGet]
[Authorize()] // if not authorized, the fallback is the default, OIDC-Cookie auth
public IActionResult ShowToken()
{
// Only for demonstration purposes
var tokenViewModel = TempData.Deserialize<TokenViewModel>("tokenInfo");
return View(tokenViewModel);
}
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
var httpClient = new HttpClient(new TestClientMessageHandler("starling-testclient", typeof(Program).Assembly.GetName().Version));
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
issuer + "/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever(httpClient));
var signingKeyProvider = new DiscoveryDocumentSigningKeyProvider(configurationManager);
var tokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = issuer, // configured above
ValidateAudience = true,
ValidAudience = audience, // configured above
RequireSignedTokens = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeyResolver = (token, securityToken, keyId, validationParameters) =>
{
var signingKeys = signingKeyProvider.GetSigningKeysAsync().GetAwaiter().GetResult();
return signingKeys.Where(x => x.KeyId == keyId);
},
NameClaimType = JwtRegisteredClaimNames.Name,
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(2.0)
};
options.TokenHandlers.Add(new StrictTokenHandler()); // custom class, not required
options.TokenValidationParameters = tokenValidationParameters;
options.SaveToken = true;
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var token = context.SecurityToken;
Debug.Assert(token != null);
var principal = context.Principal;
Debug.Assert(principal?.Identity?.IsAuthenticated == true);
return Task.CompletedTask;
},
};
})
.AddCookie(options =>
{
options.LoginPath = new PathString("/Account/Login");
})
.AddOpenIdConnect(options =>
{
options.Authority = issuer;
options.SaveTokens = true;
// details omitted for brevity
});
After retrieving the token, simply read it and create a new authenticated ClaimsPrincipal like this:
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(tokenResponse.AccessToken);
var identity = new ClaimsIdentity(jwtSecurityToken.Claims, "auth-type-token");
var principal = new ClaimsPrincipal(identity);
Note: It's important to set the AuthenticationType
parameter when creating the ClaimsIdentity
. Otherwise, IsAuthenticated
will be false.
After creating ClaimsPrincipal
, call SignInAsync()
:
await HttpContext.SignInAsync(principal);
At this point, a standard ASP.NET auth cookie is used.