Reputation: 6160
I have a mobile & server side (web w/ API) application that uses Identity to authenticate the user. The mobile app opens a WebView on the server, once the user successfully logs in, the mobile app is re-opened with an access token sent back from the server. It can then utilize API's on the server side that require Bearer token authentication.
This pattern is explained here.
When using the mobile app, the Bearer token works if the user logged in via user/pass. If the user logged in via google, the API returns a 401 (access denied) error. Here is my auth setup code:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
})
// I tried .AddGoogle here previously, but this is the only way I've been able to return an id_token. The access_token is for google API's, not internal API's
.AddOpenIdConnect(
GoogleDefaults.AuthenticationScheme,
GoogleDefaults.DisplayName, o =>
{
var googleAuthNSection = configuration.GetSection("Authentication:Google");
o.SignInScheme = IdentityConstants.ExternalScheme;
o.Authority = "https://accounts.google.com";
o.ClientId = googleAuthNSection["ClientId"]!;
o.ClientSecret = googleAuthNSection["ClientSecret"]!;
o.ResponseType = OpenIdConnectResponseType.IdToken;
o.CallbackPath = "/signin-google";
o.SaveTokens = true;
o.Scope.Add("email");
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddBearerToken(IdentityConstants.BearerScheme, o =>
{
o.BearerTokenExpiration = TimeSpan.FromHours(5);
})
.AddExternalCookie();
Here is the code that acquires the bearer token if the user logs in with a user/pass. Notice I post to the /login endpoint to get the bearer token with the current users email/password since I have it accessible here. When logging on with google I won't have a email/password so cannot re-authenticate to an identity endpoint to get a bearer token.
var body = new { email, password };
var content = JsonSerializer.Serialize(body);
var buffer = Encoding.UTF8.GetBytes(content);
var byteContent = new ByteArrayContent(buffer);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var client = httpClientFactory.CreateClient(MealPlanApi.ApiClient);
var loginResponse = await client.PostAsync("/login", byteContent);
var contents = await loginResponse.Content.ReadAsStringAsync();
var doc = JsonSerializer.Deserialize<JsonElement>(contents);
var accessToken = doc.GetProperty("accessToken").GetString()!;
await RedirectToMobileAuthenticatedAsync(context, user, accessToken, returnUrl);
Here is the code that logs in 3rd party auth and sends the token back to the mobile app. The user is obviously authenticated, but the access token acquired here does not work for accessing the APIs.
[Route("mobileauth")]
[ApiController]
public class AuthController : ControllerBase
{
const string callbackScheme = "MyCallbackScheme";
[HttpGet("{scheme}")] // eg: Microsoft, Facebook, Apple, etc
public async Task Get([FromRoute] string scheme)
{
var auth = await Request.HttpContext.AuthenticateAsync(scheme);
if (!auth.Succeeded
|| auth?.Principal == null
|| !auth.Principal.Identities.Any(id => id.IsAuthenticated)
|| string.IsNullOrEmpty(auth.Properties.GetTokenValue("access_token")))
{
// Not authenticated, challenge
await Request.HttpContext.ChallengeAsync(scheme);
}
else
{
var claims = auth.Principal.Identities.FirstOrDefault()?.Claims;
var email = string.Empty;
email = claims?.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
// Get parameters to send back to the callback
var qs = new Dictionary<string, string>
{
{ "access_token", auth.Properties.GetTokenValue("access_token") },
{ "refresh_token", auth.Properties.GetTokenValue("refresh_token") ?? string.Empty },
{ "expires", (auth.Properties.ExpiresUtc?.ToUnixTimeSeconds() ?? -1).ToString() },
{ "email", email }
};
// Build the result url
var url = callbackScheme + "://#" + string.Join(
"&",
qs.Where(kvp => !string.IsNullOrEmpty(kvp.Value) && kvp.Value != "-1")
.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
// Redirect to final url
Request.HttpContext.Response.Redirect(url);
}
}
}
Is this perhaps a problem with how I'm securing and using the endpoints?
In Program Main
app.MapControllers();
Controller
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Identity.Bearer,Identity.External,Google")]
[ApiController]
public class MyController() : ControllerBase
Here is my response from HttpContext.AuthenticateAsync("Google")
Notice I am authenticated. The current user can access resources on the web. However, if I pass this access token or the ID token to the mobile app, it is not useable for the API.
Upvotes: 1
Views: 171
Reputation: 6160
.AddOpenIdConnect
is needed for the web (to authenticate)
.AddJwtBearer
is needed for the API (to give access to the APIs)
If these are two separate applications these would go in the two separate startups. In my case, the web app and the API are running in the same service, so these go together.
.AddOpenIdConnect(
GoogleDefaults.AuthenticationScheme,
GoogleDefaults.DisplayName, o =>
{
var googleAuthNSection = configuration.GetSection("Authentication:Google");
o.SignInScheme = IdentityConstants.ExternalScheme;
o.Authority = "https://accounts.google.com";
o.ClientId = googleAuthNSection["ClientId"]!;
o.ClientSecret = googleAuthNSection["ClientSecret"]!;
o.ResponseType = OpenIdConnectResponseType.CodeToken;
o.CallbackPath = "/signin-google";
o.SaveTokens = true;
o.Scope.Add("email");
})
.AddJwtBearer(o =>
{
var googleAuthNSection = configuration.GetSection("Authentication:Google");
var googleIssuer = "https://accounts.google.com";
var googleClientId = googleAuthNSection["ClientId"]!;
o.Authority = googleIssuer;
o.Audience = googleClientId;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = false,
ValidIssuer = googleIssuer,
ValidAudience = googleClientId,
};
})
The API is then secured with the "Bearer" scheme
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
[ApiController]
public class MyController : ControllerBase
The access token is the id_token
returned from OAuth with the callback
private async Task OnLoginCallbackAsync()
{
// Sign in the user with this external login provider if the user already has a login.
var result = await SignInManager.ExternalLoginSignInAsync(
externalLoginInfo.LoginProvider,
externalLoginInfo.ProviderKey,
isPersistent: true,
bypassTwoFactor: true);
var email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? string.Empty;
if (result.Succeeded)
{
Logger.LogInformation(
"{Name} logged in with {LoginProvider} provider.",
externalLoginInfo.Principal.Identity?.Name,
externalLoginInfo.LoginProvider);
var auth = await HttpContext.AuthenticateAsync(externalLoginInfo.LoginProvider);
var token = auth.Properties.GetTokenValue("id_token");
// token can be passed back to the mobile app
Upvotes: 1