I have implemented azure ad authentication successfully. I am able to
login, and display the user's name.
I now need to call the graph api to access the user's email address.
I have my token type set to "ID" tokens in the azure portal.
Index.Razor
Code {
private HttpClient _httpClient;
public string Name { get; set; }
public string userDisplayName = "";
//this is what I am using to get the user's name
protected override async Task OnInitializedAsync()
{
var authstate = await Authentication_.GetAuthenticationStateAsync();
var user = authstate.User.Identity.Name;
if (user != null)
{
Name = user;
// 1) this is what I'm trying to use right now.
//The Graph API SDK
var attempt= await GraphServiceClient.Me.Request().GetAsync();
}
else
{
Name = "";
}
/*
// 2)this is what I've tried to use to access the graph api
_httpClient = HttpClientFactory.CreateClient();
// get a token
var token = await TokenAcquisitionService.GetAccessTokenForUserAsync(new string[] { "User.Read" });
// make API call
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var dataRequest = await _httpClient.GetAsync("https://graph.microsoft.com/beta/me");
if (dataRequest.IsSuccessStatusCode)
{
var userData = System.Text.Json.JsonDocument.Parse(await dataRequest.Content.ReadAsStreamAsync());
userDisplayName = userData.RootElement.GetProperty("displayName").GetString();
}
}
Startup.cs
var initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();
services.AddAuthorization();
services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
Appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"TenantId": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"ClientId": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"CallbackPath": "/.auth/login/aad/callback",
"ClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/beta",
"Scopes": "user.read"
},
When I attempt a request via the 1st tried method mentioned in the Index.razor file above (I commented it out with the number 1) I get an error of: "MSAL.Net No account or login hint was passed to the AcquireTokenSilent call"
More details:
This an image of my delegated permissions set in azure portal
Lastly: this is a link the example I followed. https://github.com/wmgdev/BlazorGraphApi
You can add the optional "email" claim if you have control over the Azure AD App Registration:
After doing that, you will have an "emailaddress" claim in authstate.User.Claims
I just tried it in my Blazor app and it works great. I think it is possible for there to not be an email property though, so make sure you null check, etc.
You can use #context.User.Claims to get a login name, in case of, login name is same as an email address, as usual.
< AuthorizeView>
< Authorized>
Hello, #context.User.Claims.First( cl => cl.Type.ToString()=="preferred_username").Value
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</ AuthorizeView>
Retrieving a user's login name in Blazor WASM component
Related
I'm trying to implement On-behalf-of user in Asp.net Web API (.net 5). I receive an access_token from the Mobile APP, send it to my Web API. The Web API uses this token to call the GRAPH API to get the user's profile details.
Below is my code
Startup.cs file
services.AddAuthentication("JwtBearer")
.AddJwtBearer("JwtBearer", options =>
{
options.MetadataAddress = $"https://login.microsoftonline.com/{Configuration["b2bAzureAppIdentity:TenantId"]}/v2.0/.well-known/openid-configuration";
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = $"https://sts.windows.net/{Configuration["b2bAzureAppIdentity:TenantId"]}/",
// as audience, both the client id and the identifierUri are allowed (sematically equivalent)
ValidAudiences = new[] { Configuration["b2bAzureAppIdentity:AppIdUri"], Configuration["b2bAzureAppIdentity:ClientId"] }
};
}).AddMicrosoftIdentityWebApi(Configuration, "b2bAzureAppIdentity")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
controller.cs
[HttpGet("GetMyDetails")]
[AuthorizeForScopes(Scopes = new string[] { "user.read" })]
public async Task<IActionResult> GetMyDetails()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
return new OkObjectResult(user.Photo);
}
Appsettings is in the below format
"b2bAzureAppIdentity": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "",
"TenantId": "",
"ClientId": "",
"ClientSecret": "",
"AppIdUri": ""},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read"},
In Azure, the API permissions and scope are set correctly, this is evident because when I make calls from postman, I'm able to get the accesstoken for on_behalf_of and use it to get the user's profile details by calling https://graph.microsoft.com/v1.0/me
In the controller at this line
var user = await _graphServiceClient.Me.Request().GetAsync();
I get an error: "No account or login hint was passed to the AcquireTokenSilent call. "
I have googled for this error and the solution says that the user should consent the scope however it has already been consented by the admin in Azure portal. Also, the fact that this is working in Postman makes be believe that the configurations for the APP and the API are correct.
Has anyone faced similar issue?
This is happening because the access_token received is not sent along with the request to get the user detail. Here's an example of how to implement on behalf of provider:
// Create a client application.
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
// The Authority is a required parameter when your application is configured
// to accept authentications only from the tenant where it is registered.
.WithAuthority(authority)
.WithClientSecret(clientSecret)
.Build();
// Use the API reference to determine which scopes are appropriate for your API request.
// e.g. - https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
var scopes = new string[] { "User.Read" };
// Create an authentication provider.
ClientCredentialProvider authenticationProvider = new OnBehalfOfProvider(confidentialClientApplication, scopes);
var jsonWebToken = actionContext.Request.Headers.Authorization.Parameter;
var userAssertion = new UserAssertion(jsonWebToken);
// Configure GraphServiceClient with provider.
GraphServiceClient graphServiceClient = new GraphServiceClient(authenticationProvider);
// Make a request
var me = await graphServiceClient.Me.Request().WithUserAssertion(userAssertion).GetAsync();
In this case the token is added to the request in the call to WithUserAssertion.
Please let me know if this helps, and if you have further questions.
I'm unclear if I need to store and or use access tokens in this social login implementation.
I can read access token by configuring Startup.cs as below. I know that in many OAuth flows access tokens would be verified before use.
Using .NET Core's authentication framework however, I'm unclear how much of this work has already been done for me. I've looked through the Microsoft.AspNetCore.Authentication.Google source but this hasn't make it any clearer.
In the LoginCallback action below, I can read an ID for the user using:
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
So far as I can tell, this is all I need. I can store that identifier in the database and log the user in again if the ID matches next time they sign in via the authentication provider (Google, Facebook etc).
This is providing that the framework has verified the access token before I read that ID however, and this what I'm not clear on. Is the framework validating the access token before setting the values on the AuthenticateResult so that I can trust it has not been tampered with?
If so, I don't see any need to store access tokens.
Startup.cs
services.AddAuthentication(o =>
{
o.DefaultScheme = "Application";
o.DefaultSignInScheme = "External";
})
.AddCookie("Application")
.AddCookie("External")
.AddGoogle(o =>
{
o.ClientId = "xxxxxxxxxxx";
o.ClientSecret = "xxxxxxxxxxx";
// Can access tokens by configuring as follows
o.SaveTokens = true;
o.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
}) // Other providers ...
Callback handing
[Route("oauth/google-challenge")]
public IActionResult GoogleLoginChallenge(string returnUrl)
{
return new ChallengeResult(
GoogleDefaults.AuthenticationScheme,
new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl, oauthProvider = OAuthProvider.Google })
});
}
[Route("oauth/facebook-challenge")]
public IActionResult FacebookLoginChallenge(string returnUrl)
{
return new ChallengeResult(
FacebookDefaults.AuthenticationScheme,
new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl, oauthProvider = OAuthProvider.Facebook })
});
}
[Route("oauth/microsoft-challenge")]
public IActionResult MicrosoftLoginChallenge(string returnUrl)
{
return new ChallengeResult(
MicrosoftAccountDefaults.AuthenticationScheme,
new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl, oauthProvider = OAuthProvider.MicrosoftAccount })
});
}
[Route("oauth/login-callback")]
public async Task<IActionResult> LoginCallback(string returnUrl, OAuthProvider oAuthProvider)
{
var authenticateResult = await HttpContext.AuthenticateAsync("External").ConfigureAwait(false);
if (!authenticateResult.Succeeded)
{
return BadRequest();
}
var claimsIdentity = new ClaimsIdentity("Application");
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.Email));
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.Name));
foreach (KeyValuePair<string,string> item in authenticateResult.Properties.Items)
{
// Read access tokens here
}
foreach (KeyValuePair<string,string> item in authenticateResult.Ticket.Properties.Items)
{
// or here
}
string accessToken = await HttpContext.GetTokenAsync("access_token");
return Redirect(returnUrl);
}
As I wasn't clear what actions the framework was taking to verify that the user information was from the authentication provider, I set up requests from the .NET core app to go via Fiddler.
"environmentVariables": {
...
"ALL_PROXY": "http://127.0.0.1:8888"
},
In the case of the Google OAuth provider, following the redirect of the browser back to the signin-google URI, I was able to see the framework make calls back to:
www.googleapis.com/oauth2/v4/token, sending the client ID secret and some other information to obtain an access and ID token.
It then calls to:
www.googleapis.com/oauth2/v4/userinfo passing the access and ID token.
{
"id": "xxxx",
"email": "xxxx#xxxx.com",
"verified_email": true,
"name": "Example",
"given_name": "Example",
"family_name": "Example",
"picture": "https://lh3.googleusercontent.com/a-/xxxxxx",
"locale": "en",
"hd": "xxxx.com"
}
It is clear that the tokens are short lived, so there is little point in persisting them.
The obtaining and verification of access tokens through use from server to server also looks to be secure.
I have a problem enabling Facebook auth in my ASP.NET Core web app. I'm using ASP.NET Core Authentication but not Identity. The auth is configured in Startup like this:
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddFacebook(options =>
{
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.CallbackPath = "/signinfacebookcallback";
});
As shown in the code, I want to use cookie auth, but also allow people to sign in with Facebook. Once they have been successfully signed in, I want to set the auth cookie.
To show the challenge, I have the following action:
[HttpGet]
[Route("signinfacebook")]
public ActionResult SignInFacebook()
{
return Challenge(FacebookDefaults.AuthenticationScheme);
}
This redirects the user to the Facebook login screen. Once they sign in, the user is redirected to the URL specified in config:
[HttpGet]
[Route("signinfacebookcallback")]
public async Task<ActionResult> SignInFacebookCallback()
{
var result = await HttpContext.AuthenticateAsync();
if (!result.Succeeded) return Redirect("/login/");
...
}
When I debug the code, result.Succeeded returns false and the AuthenticationResult object doesn't contain more information on why Succeeded is false.
I verified that the app id and secret are correct.
What could be the issue here?
The CallbackPath in the OpenID Connect middleware is internal path that are used for the authentication flow of the OpenID Connect protocol. After Identity provider redirect user to that url in your application , middeware will handle token valiation ,token decode,exchange token and finally fill the user principle , and that process is fired before your controller gets involved .
Since CallbackPath is internal and will handled by OpenID Connect middleware automatically , you don't need to care about , make sure the callback is registered in facebook's allowed redirect url and let middleware handle the callback .If you want to redirect user to specific route/page after authentication , put the url to AuthenticationProperties :
if (!User.Identity.IsAuthenticated)
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/home/Privacy" } ,FacebookDefaults.AuthenticationScheme);
}
And you should remove the callback path route (signinfacebookcallback) in your application .
UPDATE
If you want to access database and manage local user , you can use built-in events in middleware, for AddFacebook middleware , you can use OnTicketReceived to add access database , manage users and add claims to user's princple :
.AddFacebook(options =>
{
options.ClientId = "xxxxxxxxxxxxx";
options.ClientSecret = "xxxxxxxxxxxxxxxxxxxx";
options.CallbackPath = "/signinfacebookcallback";
options.Events = new OAuthEvents
{
OnTicketReceived = ctx =>
{
//query the database to get the role
var db = ctx.HttpContext.RequestServices.GetRequiredService<YourDbContext>();
// add claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "Admin")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
},
};
});
I have the following code to configure the process of authentication in my ASP.NET MVC web application, including the setting a claim to the user's "name" when validating the user's identity token received by the application.
(Note that I am following this sample code)
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
CookieSecure = CookieSecureOption.Always
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(AadInstance, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
Authority = Authority,
PostLogoutRedirectUri = RedirectUri,
RedirectUri = RedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
},
// Specify the claims to validate
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"{OpenIdConnectScopes.OpenId} {ReadTasksScope} {WriteTasksScope}"
});
}
The "name" claim type maps to the user's DisplayName, which is returned when I use the code User.Identity.Name.
How can I get User.Identity.Name to map to the user's Username, like in the below screenshot of an Azure Active Directory B2C user?
Here is the 2nd half of the answer above, which includes the code changes that were made:
Add the line of code commented with "Added line here" so that the user's ObjectId is claimed:
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
// Extract the code from the response notification
var code = notification.Code;
string signedInUserID = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(ClientId, Authority, RedirectUri, new ClientCredential(ClientSecret), userTokenCache, null);
///////////////////////////////////
// Added line here
///////////////////////////////////
// Add a custom claim to the user's ObjectId ('oid' in the token); Access it with this code: ((System.Security.Claims.ClaimsIdentity)User.Identity).FindFirst("ObjectId").Value
notification.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim("ObjectId", signedInUserID));
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, Scopes);
}
catch (Exception ex)
{
MyLogger.LogTrace("Failed to retrieve AuthenticationResult Token for user " + signedInUserID, MyLogger.LogLevel.Critical);
return;
}
}
Then later in the web application, when you need to get and use the user's ObjectId, do this:
try
{
string signedInUserObjectId = ((System.Security.Claims.ClaimsIdentity)User.Identity).FindFirst("ObjectId").Value;
}
catch (Exception e)
{
... This should never happen, but better safe than sorry ...
}
And lastly, using the Azure AD graph client, you can get the user object using ObjectId, which contains the user name. The specific query you will need is GET https://graph.windows.net/myorganization/users/{user_id}?api-version. You may need to get the UserPrincipalName or a SignInName, depending on your type of user. For more information, see the "Get a user" section here.
I'm not sure how you can get the username into that property as B2C does not return that value.
You can still get this value but it will take more work. B2C does allow you to return the "User's Object ID" which will come back as claim oid. Sample Token.
You can get the oid claim and then query Azure AD to get the username value. See this SO answer on querying Azure AD.
Return User's Object ID
Azure feedback item: include username in JWT claims
I am working on a sample application for OpenIddict using AngularJs. I was told that you shouldnt use clientside frameworks like Satellizer, as this isnt recommended, but instead allow the server to deal with logging in server side (locally and using external login providers), and return the access token.
Well i have a demo angularJs application and uses server side login logic and calls back to the angular app, but my problem is, how do i get the access token for the current user?
here is my startup.cs file, so you can see my configuration so far
public void ConfigureServices(IServiceCollection services) {
var configuration = new ConfigurationBuilder()
.AddJsonFile("config.json")
.AddEnvironmentVariables()
.Build();
services.AddMvc();
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration["Data:DefaultConnection:ConnectionString"]));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddOpenIddict();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
env.EnvironmentName = "Development";
var factory = app.ApplicationServices.GetRequiredService<ILoggerFactory>();
factory.AddConsole();
factory.AddDebug();
app.UseDeveloperExceptionPage();
app.UseIISPlatformHandler(options => {
options.FlowWindowsAuthentication = false;
});
app.UseOverrideHeaders(options => {
options.ForwardedOptions = ForwardedHeaders.All;
});
app.UseStaticFiles();
// Add a middleware used to validate access
// tokens and protect the API endpoints.
app.UseOAuthValidation();
// comment this out and you get an error saying
// InvalidOperationException: No authentication handler is configured to handle the scheme: Microsoft.AspNet.Identity.External
app.UseIdentity();
// TOO: Remove
app.UseGoogleAuthentication(options => {
options.ClientId = "XXX";
options.ClientSecret = "XXX";
});
app.UseTwitterAuthentication(options => {
options.ConsumerKey = "XXX";
options.ConsumerSecret = "XXX";
});
// Note: OpenIddict must be added after
// ASP.NET Identity and the external providers.
app.UseOpenIddict(options =>
{
options.Options.AllowInsecureHttp = true;
options.Options.UseJwtTokens();
});
app.UseMvcWithDefaultRoute();
using (var context = app.ApplicationServices.GetRequiredService<ApplicationDbContext>()) {
context.Database.EnsureCreated();
// Add Mvc.Client to the known applications.
if (!context.Applications.Any()) {
context.Applications.Add(new Application {
Id = "myClient",
DisplayName = "My client application",
RedirectUri = "http://localhost:5000/signin",
LogoutRedirectUri = "http://localhost:5000/",
Secret = Crypto.HashPassword("secret_secret_secret"),
Type = OpenIddictConstants.ApplicationTypes.Confidential
});
context.SaveChanges();
}
}
}
Now my AccountController is basically the same as the normal Account Controller, although once users have logged in (using local and external signin) i use this function and need an accessToken.
private IActionResult RedirectToAngular()
{
// I need the accessToken here
return RedirectToAction(nameof(AccountController.Angular), new { accessToken = token });
}
As you can see from the ExternalLoginCallback method on the AccountController
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null)
{
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
// SHOULDNT THE USER HAVE A LOCAL ACCESS TOKEN NOW??
return RedirectToAngular();
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else {
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
// SHOULDNT THE USER HAVE A LOCAL ACCESS TOKEN NOW??
return RedirectToAngular();
}
That's not how it's supposed to work. Here's what happens in the classical flow:
The OAuth2/OpenID Connect client application (in your case, your Satellizer JS app) redirects the user agent to the authorization endpoint (/connect/authorize by default in OpenIddict) with all the mandatory parameters: client_id, redirect_uri (mandatory in OpenID Connect), response_type and nonce when using the implicit flow (i.e response_type=id_token token). Satellizer should do that for you, assuming you've correctly registered your authorization server (1).
If the user is not already logged in, the authorization endpoint redirects the user to the login endpoint (in OpenIddict, it's done for you by an internal controller). At this point, your AccountController.Login action is invoked and the user is displayed a login form.
When the user is logged in (after a registration process and/or an external authentication association), he/she MUST be redirected back to the authorization endpoint: you can't redirect the user agent to your Angular app at this stage. Undo the changes made to ExternalLoginCallback and it should work.
Then, the user is displayed a consent form indicating he/she's about to allow your JS app to access his personal data on his/her behalf. When the user submits the consent form, the request is handled by OpenIddict, an access token is generated and the user agent is redirected back to the JS client app, with the token appended to the URI fragment.
[1]: according to the Satellizer documentation, it should be something like that:
$authProvider.oauth2({
name: 'openiddict',
clientId: 'myClient',
redirectUri: window.location.origin + '/done',
authorizationEndpoint: window.location.origin + '/connect/authorize',
responseType: 'id_token token',
scope: ['openid'],
requiredUrlParams: ['scope', 'nonce'],
nonce: function() { return "TODO: implement appropriate nonce generation and validation"; },
popupOptions: { width: 1028, height: 529 }
});