Cannot sign in with different account or "Use another account" - azure-active-directory

I'm trying to integrate Microsoft sso with a Xamarin.Forms app.
I'm using Microsoft.Identity.Client 4.7.1
I struggling to sign in with different accounts on the same device since it seems that the first account is always picked no matter what I do.
User A signs in
User A signs out
User B enters the app opens the webview with the Microsoft login page and prompts the "Use another account" button but even after typing his account, the webview redirects it to back to the mobile app as user A.
Here's the code that handles sign-in and sing-out:
private IPublicClientApplication _publicClientApplication;
public AuthService()
{
_publicClientApplication = PublicClientApplicationBuilder.Create(Constants.MicrosoftAuthConstants.ClientId.Value)
.WithAdfsAuthority(Constants.MicrosoftAuthConstants.Authority.Value)
.WithRedirectUri(Constants.MicrosoftAuthConstants.RedirectUri.Value)
.Build();
}
public async Task<string> SignInAsync()
{
var authScopes = Constants.MicrosoftAuthConstants.Scopes.Value;
AuthenticationResult authResult;
try
{
// call to _publicClientApplication.AcquireTokenSilent
authResult = await GetAuthResultSilentlyAsync();
}
catch (MsalUiRequiredException)
{
authResult = await _publicClientApplication.AcquireTokenInteractive(authScopes)
.WithParentActivityOrWindow(App.ParentWindow)
.ExecuteAsync();
}
return authResult.AccessToken;
}
private async Task<IAccount> GetCachedAccountAsync() => (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault();
public async Task SignOutAsync()
{
var firstCachedAccount = await GetCachedAccountAsync();
await _publicClientApplication.RemoveAsync(firstCachedAccount);
}
A workaround is to use Prompt.ForceLogin but what's the point of sso if you have to type the credentials every time.

The line of code await _publicClientApplication.RemoveAsync(firstCachedAccount); can jsut remove the user from the cache, it doesn't implement a signout method. So you need to do logout manually by the api below:
https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=https://localhost/myapp/

Related

Using a blazor server with signalR as a relay server

The goal is to use a Blazor server as a relay server using signalR.
I have little to no experience with blazor servers before this.
The Idea would be to connect a Winform/Xamarin client to this server, target the recipient using a name/id from an existing database, and relay the necessary info.
Hub:
[Authorize]
public class ChatHub : Hub
{
public Task SendMessageAsync(string user, string message)
{
//Context.UserIdentifier
Debug.WriteLine(Context.UserIdentifier);
Debug.WriteLine(Context?.User?.Claims.FirstOrDefault());
return Clients.All.SendAsync("ReceiveMessage", user, message); ;
}
public Task DirectMessage(string user, string message)
{
return Clients.User(user).SendAsync("ReceiveMessage", user, message);
}
}
As per documentation I'm trying to set the Context.UserIdentifier, I do however struggle with the authentication part. My program.cs looks like this:
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddTransient<IUserIdProvider, MyUserIdProvider>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
//var accessToken = context.Request.Query["access_token"];
var accessToken = context.Request.Headers["Authorization"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chathub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSignalR();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.MapBlazorHub();
app.MapHub<ChatHub>("/chathub");
app.MapFallbackToPage("/_Host");
app.Run();
As for my Client (a winform test client) I tried something like this:
HubConnection chatHubConnection;
chatHubConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7109/chathub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
private async void HubConBtn_Click(object sender, EventArgs e)
{
chatHubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Invoke(() =>
{
var newMessage = $"{user}: {message}";
MessagesLB.Items.Add(newMessage);
});
});
try
{
await chatHubConnection.StartAsync();
MessagesLB.Items.Add("Connected!");
HubConBtn.Enabled = false;
SendMessageBtn.Enabled = true;
}
catch (Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
As a first step I'm just trying to authenticate a user/check that it's in the live database, if so connect and fill out: Context.UserIdentifier so I can use this within the Hub. I understand that I probably need a middleware however I don't really know exactly how to test a connectionId/Jwt token or similar to get the user/connection.
Any nudge in the right direction would be appreciated.
If I understand your question you don't know where and how to generate a JWT token.
For me the JWT token should be generated from the server, your hub.
POST api/auth and in the playload you give login + SHA256 password and returns JWT token.
Once you checked the user auth is correct in you DB you can issue the token.
To generate a JWT token I use this piece of code.
public string GenerateToken(IConfiguration Config, DateTime? expire)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.Jti, _id),
new Claim(ClaimsIdentity.DefaultRoleClaimType, role)
};
// ClaimsIdentity.DefaultRoleClaimType
var bytes = Encoding.UTF8.GetBytes(Config["jwt:Secret"]);
var key = new SymmetricSecurityKey(bytes);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
var token = new JwtSecurityToken(
//Config.GetValue<string>("jwt:Issuer"),
//Config.GetValue<string>("jwt:Issuer") + "/ressources",
claims: claims,
expires: DateTime.Now.AddMinutes(Config.GetValue<int>("jwt:ExpireMinute")),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
#edit
Look here to allow JWT for SignalR
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-6.0
I also added this.
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
The easiest solution would be to use something like IdentityServer to handle the authentication. It's a free solution, also .NET based which takes very little configuration effort to offer you simple client credentials authentication and generate the token for you.
I did basically exactly what you're asking here: A WinForms application connecting to my signalR hub application on a remote server, using Bearer token - but I also have OIDC/OAUTH implemented with third party user account login.
IdentityServer offers a great repository of full examples that showing you all the flow - and with just a few lines of code changed, you have a fullblown authentication system, which can be enhanced easily.
With IdentityServer you get everything, even the corresponding extension methods that enable your signalR hub application to create the claims principal (aka user) from the claims included within your token.
Here you'll find all the examples and docs:
https://github.com/IdentityServer/IdentityServer4
If you hit any walls, just reply here and I'll try to help.

IdentityServer4 Windows Authentication Missing Callback implementation

The documentation to setup Windows Authentication is here: https://docs.identityserver.io/en/latest/topics/windows.html
But I have no idea how to configure the Callback() method referred to in the line RedirectUri = Url.Action("Callback"), or wethere or not I'm even supposed to use that.
I tried manually redirecting back to the https://<client:port>/auth-callback route of my angular app but I get the error:
Error: No state in response
at UserManager.processSigninResponse (oidc-client.js:8308)
Does someone have a suggested Callback method I can use with an SPA using code + pkce ? I've tried searching Google but there are no current example apps using Windows Authentication and the ones that do exist are old.
Take a look at the ExternalLoginCallback method. I've also pasted the version of the code as of 26 Oct 2020 below for future reference incase the repo goes away.
/// <summary>
/// Post processing of external authentication
/// </summary>
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = await AutoProvisionUserAsync(provider, providerUserId, claims);
}
// this allows us to collect any additonal claims or properties
// for the specific prtotocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
additionalLocalClaims.AddRange(claims);
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps);
ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
// we must issue the cookie maually, and can't use the SignInManager because
// it doesn't expose an API to issue additional claims from the login workflow
var principal = await _signInManager.CreateUserPrincipalAsync(user);
additionalLocalClaims.AddRange(principal.Claims);
var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id;
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, name));
// issue authentication cookie for user
var isuser = new IdentityServerUser(principal.GetSubjectId())
{
DisplayName = name,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
// validate return URL and redirect back to authorization endpoint or a local page
var returnUrl = result.Properties.Items["returnUrl"];
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
}

Authorization Flow Access and Refresh Tokens

Using Authorization Code does the middleware that intercepts signin-oidc exchange the authorization code for the access tokens or do I have to do this programatically? If the middleware does it, then were can I find the access and refresh tokens?
Or do I have to implement my own redirect url and code and capture the returned code and exchange it with the access tokens using RequestAuthorizationCodeTokenAsync?
No you do not have to implement the part to obtain the tokens this is handled by the handler, But you need a callback to handle the signin, storing claims and creating a login. Here is a primitive example of how to Obtain the Access Tokens:
EDIT
I will use Google as an example because I have the code on hand but the IdentityServer OAuth should be the same, seeing as they Extend OAuthHandler
services.AddAuthentication(options =>
{
//Add your identity Server schema etc
})
.AddGoogle(options =>
{
options.SaveTokens = true;
options.ClientId = Configuration["Google:ClientId"];
options.ClientSecret = Configuration["Google:ClientSecret"];
})
And in your Authentication controller:
[HttpPost("ExternalLogin")]
[AllowAnonymous]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet("ExternalLoginCallback")]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
throw new Exception($"Error from external provider: {remoteError}");
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
//It throws here, since there are no tokens
throw new Exception("Error: could not find user tokens");
}
//Handle the rest of authentication
}
What Happens? You have a button pointing to your External Login Provider "Google" as the provider.
You're redirected to the Google login page, and you login.
Google server redirects you back to you're domain and /google-signin (by default hidden in the handle) With the Authorization Code
The Google handler then uses the authorization code along with your secret to obtain the tokens
If you specify to save Tokens, in the OAuth Options, Tokens from the response will be saved. Along with some basic claims obtained from the user info endpoint.
You're then redirected to the External Login callback:
_signInManager.GetExternalLoginInfoAsync();
Will obtain the saved tokens.
So to answer your question. The handler will take care of saving tokens (If you specify it to). And you can obtain them from the signInManger if needed.

How can I prompt user for more information during sign-in/registration process with Firebase?

I am using the firebaseUI toolset to allow users to login to my app.
I'm also using the firebase.auth().onAuthStateChanged method to redirect them to the main application once they are logged in.
What I would like to do is, during the sign-in process...if they are a new user, prompt them for a displayname to use (to override whatever name is given by the 3rd party provider).
Right now, once they login, they are redirected to the app..and it's not obvious how I can have a smooth workflow that would provide a modal /screen to allow them to create a display name before continuing onto the app.
Is there a recommended best practice of how to capture/prompt for additional information during the first sign-in process, which I would then store in my firebase /users collection?
Right now it will just auto redirect to the main app.
componentDidMount() {
//console.log(this.props.isLoading)
firebase.auth().onAuthStateChanged(user => {
if(user) {
this.props.setUser(user);
this.props.history.push('/');
}
callbacks : {
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
var user = authResult.user;
var credential = authResult.credential;
var isNewUser = authResult.additionalUserInfo.isNewUser;
var providerId = authResult.additionalUserInfo.providerId;
var operationType = authResult.operationType;
if(isNewUser)
{
//prompt user to enter a displayName to use with the application
}
// Do something with the returned AuthResult.
// Return type determines whether we continue the redirect automatically
// or whether we leave that to developer to handle.
return true;
},
},

Microsoft graph API: getting 403 while trying to read user groups

I am trying to get user's group information who log-Ins into the application.
Using below code, when I am hitting https://graph.microsoft.com/v1.0/users/{user}, then I am able to see that user is exist (200), but when trying to hit https://graph.microsoft.com/v1.0/users/{user}/memberOf, then I am getting 403.
private static async Task Test()
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "TOKEN HERE");
var user = "testuser#onmicrosoft.com";
var userExist = await DoesUserExistsAsync(client, user);
Console.WriteLine($"Does user exists? {userExist}");
if (userExist)
{
var groups = await GetUserGroupsAsync(client, user);
foreach (var g in groups)
{
Console.WriteLine($"Group: {g}");
}
}
}
}
private static async Task<bool> DoesUserExistsAsync(HttpClient client, string user)
{
var payload = await client.GetStringAsync($"https://graph.microsoft.com/v1.0/users/{user}");
return true;
}
private static async Task<string[]> GetUserGroupsAsync(HttpClient client, string user)
{
var payload = await client.GetStringAsync($"https://graph.microsoft.com/v1.0/users/{user}/memberOf");
var obj = JsonConvert.DeserializeObject<JObject>(payload);
var groupDescription = from g in obj["value"]
select g["displayName"].Value<string>();
return groupDescription.ToArray();
}
Is this something related to permission issue, my token has below scope now,
Note - Over here I am not trying to access other user/group information, only who log-ins. Thanks!
Calling /v1.0/users/[a user]/memberOf requires your access token to have either Directory.Read.All, Directory.ReadWrite.All or Directory.AccessAsUser.All and this is
documented at https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_list_memberof.
A great way to test this API call before implementing it in code is to use the Microsoft Graph explorer where you can change which permissions your token has by using the "modify permissions" dialog.

Resources