Cookie lost on Azure B2C login/logout - redis

I have a .net 6 MVC application where I am authenticating with Azure B2C and storing the token in local Docker Redis instance.
Data protection keys are also stored in Redis.
The issue I have is that when I either log in or out something is happening with the session and I am loosing cookies that I previous set. For example a cookie that hold the user acceptance for non essential cookies.
Setting up Redis
var redisConnection = configuration.GetConnectionString("RedisConnection");
services
.AddDataProtection()
.PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(redisConnection), "EPR_AC_DataProtectionKeys");
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnection;
options.InstanceName = configuration.GetValue<string>("RedisInstanceName");
});
services.AddSession(options =>
{
options.Cookie.Name = configuration.GetValue<string>("CookieOptions:SessionCookieName");
options.IdleTimeout = TimeSpan.FromMinutes(configuration.GetValue<int>("SessionIdleTimeOutMinutes"));
options.Cookie.HttpOnly = true;
});
Setting up token authentication
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAdB2C"))
.EnableTokenAcquisitionToCallDownstreamApi(new string[] {"https://dummytenant.onmicrosoft.com/myapp-uat/access"})
.AddDistributedTokenCaches();

Related

Client application (oidc) is redirected to login when IdentityServer4 is restarted

I have an implementation of IdentityServer with an external Provider to authenticate and a client application that implements oidc implicit flow with the oidc-client library.
All works correctly (login, logout, refresh token, ecc…) until the IdentityServer shut down (for example due to inactivity).
When the IdentityServer is restarted my client is redirected to login also if the session/token is still valid. I tried with either OperationalStore or InMemoryPersistedGrants but, after the restart of the service and without closing the browser, I have always the same behavior.
It’s like if any session/token is stored on database with the OperationalStore enabled. The cookie instead is correctly persisted by browser session and permit to navigate through the IdentityServer pages after the restart without asking for login.
The API call (from client) that redirect to login, after the service restart, is this one:
IdS-server/connect/authorize?client_id=client-id&redirect_uri=redirectUri&response_type=code&scope=scopes&state=state&code_challenge=codeChallenge &code_challenge_method=S256&response_mode=query
Is there some missing or wrong configuration in my IdentityServer?
//
// IdentityServerBuilder
//
identityServerBuilder = services.AddIdentityServer();
// this adds the operational data from DB (codes, tokens, consents)
identityServerBuilder.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
identityServerBuilder.AddInMemoryIdentityResources(identityServerService.GetIdentityResources())
.AddInMemoryApiScopes(identityServerService.GetApiScopes())
.AddInMemoryApiResources(identityServerService.GetApiResources())
.AddInMemoryClients(identityServerService.GetClients());
identityServerBuilder.AddTestUsers(identityServerService.GetUsers().ToTestUsers().ToList());
identityServerBuilder.AddDeveloperSigningCredential();
//
// AuthenticationBuilder
//
var cookieScheme = identityServerConfig.GetCookieSchemeOrDefault();
if (identityServerConfig.IsCookieSchemeCustom())
{
authenticationBuilder = services.AddAuthentication(cookieScheme).AddCookie(cookieScheme, options =>
{
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
});
}
else
{
authenticationBuilder = services.AddAuthentication().AddCookie(options =>
{
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
});
}
Because you use this statement in production:
AddDeveloperSigningCredential()
This means that the token signing key will be recreated if you don't remember it across development. A best practice it to not use this statement in production and instead add a more permanent signing key, perhaps in a database, or in Azure Key Vault or somewhere else where it can be stored safely.
A second thing is that you also need to configure is the Data Protection API and pass it an encryption key that is also persisted outside your system. This is a similar thing to the signing key issue. I did a blog post about it here. The encryption key must be the same across deployment, otherwise the users will be signed out after redeployment because their cookies will no longer be accepted.

Not getting .AspNetCore.AzureADCookie when authenticating postman against Azure Active Directory

We are developing a web application communicating with its backend API. API is written in .NET Core and is running in Azure and is using OpenID authentication against Azure Active Directory. Configuration of the authentication process is below (as you can see we're using cookie based authentication):
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = AzureADDefaults.CookieScheme;
options.DefaultChallengeScheme = AzureADDefaults.AuthenticationScheme;
options.DefaultSignInScheme = AzureADDefaults.CookieScheme;
})
.AddAzureAD(options =>
{
configuration.Bind("AzureAd", options);
});
services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.MaxAge = new TimeSpan(7, 0, 0, 0);
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
});
We want to test our application API in Postman and we have set up a request with authentication against AAD (configuration below). Postman is able to make it through authentication and we get the access_token, however the request to API fails.
When we compared Postman cookies and browser cookies we discovered, that browser contains a cookie postman is missing .AspNetCore.AzureADCookie. It's Friday afternoon and we really got into desperation phase and have no clue what may be wrong. How can we make Postman to call AAD in a way it returns such cookie in response and adds it to the API request.
You should be able to use your browser cookies by installing Postman Interceptor extension.
Please try the same and let me know if it works.
https://learning.postman.com/docs/sending-requests/capturing-request-data/interceptor/#syncing-cookies
You can also ref the following ->
Postman is not using cookie

Is it possible to implement Row Level Security by User in Azure SQL DB and Access the User Specific Rowset on ASP.Net 3.1 Web App

I have an Azure SQL DB and a set of users who need remote/mobile access to data in this database. I would like to implement Row Level Security to return a filtered subset to each user.
Row Level Security I can handle, but am stuck trying to figure out whether a ASP.Net Core 3.1 Web App can forward the user authentication to Azure SQL DB or only authenticate the web app on the Azure SQL DB.
The only information that I have found is to use a Managed Identity to connect which grants the App access to the DB and effectively bypasses the user's credentials.
Is this actually possible to achieve? How do I sign in the user to the DB through the Web App using AzureAD authentication?
Via my DbContext, this works for App Authentication
var conn = (Microsoft.Data.SqlClient.SqlConnection)Database.GetDbConnection();
conn.AccessToken =
(new Microsoft.Azure.Services.AppAuthentication.AzureServiceTokenProvider())
.GetAccessTokenAsync("https://database.windows.net/")
.Result;
But if I try to use AzureAD authentication, I get an error: SqlException: Login failed for user
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => config.Bind("AzureAd", options));
According to mu understanding, you want to use Azure AD users to login in Azure SQL. If so, please refer to the following steps
Add Azure AD user as database user
Create Azure AD application
Configure permissions
Configure Azure AD auth
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<AzureADOptions>(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.Scope.Add("https://database.windows.net//.default"); // get token fro azure sql
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.Events.OnAuthorizationCodeReceived = async context =>
{
var request = context.HttpContext.Request;
string currentUri = UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
options.CallbackPath);
var code = context.ProtocolMessage.Code;
string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder
.Create(options.ClientId)
.WithClientSecret(options.ClientSecret)
.WithRedirectUri(currentUri)
.WithAuthority(options.Authority)
.Build();
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(options.Scope, code)
.ExecuteAsync();
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
};
});
require access token
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder
.Create(ClientId)
.WithClientSecret(ClientSecret)
.WithRedirectUri(currentUri)
.WithAuthority(Authority)
.Build();
var accounts = await cca.GetAccountsAsync();
AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault())
.ExecuteAsync();
var conn = (Microsoft.Data.SqlClient.SqlConnection)new SqlConnection(ContionStr);
conn.AccessToken = result.AccessToken
For more details about how to implement Azure AD auth, please refer to the document

IdentityServer4 authenticate each client separately

I use two different clients. The IdentityServer4 provides API protections and log in form. Can I configure clients to avoid single sign on. I mean that even if I logged in the first client I need to log in the second client too.
My ID4 configuration:
internal static IEnumerable<Client> GetClients(IEnumerable<RegisteredClient> clients)
{
return clients.Select(x =>
{
var scopes = x.AllowedScopes.ToList();
scopes.Add(IdentityServerConstants.StandardScopes.OpenId);
scopes.Add(IdentityServerConstants.StandardScopes.Profile);
scopes.Add(IdentityServerConstants.StandardScopes.OfflineAccess);
var client = new Client
{
ClientId = x.Id,
ClientName = x.Name,
AllowedGrantTypes = GrantTypes.Hybrid,
RequireConsent = false,
RefreshTokenExpiration = TokenExpiration.Sliding,
RefreshTokenUsage = TokenUsage.ReUse,
ClientSecrets = {new Secret(x.Secret.Sha256())},
RedirectUris = new[] {$"{x.Url}/signin-oidc"},
PostLogoutRedirectUris = new[] {$"{x.Url}/signout-callback-oidc"},
UpdateAccessTokenClaimsOnRefresh = true,
AllowAccessTokensViaBrowser = true,
AllowedScopes = scopes,
AllowedCorsOrigins = {x.Url},
AllowOfflineAccess = true
};
return client;
});
}
All client have the same register code (Maybe it is a problem):
const string oidcScheme = "oidc";
const string coockieScheme = CookieAuthenticationDefaults.AuthenticationScheme;
services.AddAuthentication(options =>
{
options.DefaultScheme = coockieScheme;
options.DefaultChallengeScheme = oidcScheme;
})
.AddCookie(coockieScheme)
.AddOpenIdConnect(oidcScheme, options =>
{
options.SignInScheme = coockieScheme;
options.Authority = identitySettings.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = identitySettings.Id;
options.ClientSecret = identitySettings.Secret;
options.ResponseType = "code id_token";
options.Scope.Add("offline_access");
foreach (var scope in identitySettings.Scopes)
{
options.Scope.Add(scope);
}
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
});
any help will be useful.
As long as you are in the same browser session, and your apps are having the same authority (are using the same Identity Server) this will not work.
I'll explain you why - once you log in from the first client, Identity Server creates a cookie (with all the relevant data needed in it).
Now comes the second client - the authority (the Identity Server) is the same that has issued the cookie. So Identity Server recognizes your session, sees that you are already authenticated and redirects you to the second client, without asking for credentials.
After all, this is the idea of Identity Server:
IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core 2.
It enables the following features in your applications:
Authentication as a Service
Centralized login logic and workflow for all of your applications (web, native, mobile, services). IdentityServer is an officially certified implementation of OpenID Connect.
Single Sign-on / Sign-out
Single sign-on (and out) over multiple application types.
and more....
This is from the official documentation.
You have to either go for different authorities (Identity Server instances) for each client, or re-think is Identity Server the right solution for you in this case.
NOT RECOMMENDED
I'm not recommending this, because it kind of overrides the SSO idea of Identity Server, however if you still want to do it then - you can achieve what you want if you override the IProfileService. There is a method public Task IsActiveAsync(IsActiveContext context) and this context has a property IsActive which determines if the current principal is active in the current client.
You can try and implement some custom logic here, and based on the user ID (context.Subject.GetSubjectId()) and the client id (context.Client.ClientId) to determine if the user is already logged in this client or not.
EDIT
After your comment - this is something that doesn't come OOTB from Identity Server (if I can say it like this), but luckily you have an option.
Policy based authorization per client. Like this, your user can authenticate against Identity Server (and all of its clients), but only the specific clients will authorize him. You can treat this policies as a custom authorize attribute (more or less).
Like this, a user will receive unauthorized in clients, where he.. is not authorized. Hope that this clears the thing and helps :)
You can set prompt=login from all your clients.
prompt
none - no UI will be shown during the request. If this is not possible (e.g. because the user has to sign in or consent) an error is returned
login - the login UI will be shown, even if the user is already signed-in and has a valid session
https://identityserver4.readthedocs.io/en/latest/endpoints/authorize.html
This will force the second client to login again regardless of the previous client's login status.

How to use the same authorization pipeline for WebAPI with SignalR

I am adding signalr functionality to a website that only logged in users will have access to for communicating with each other. I also would like to use the same pipeline to allow azure worker roles to communicate with a user indicating job statuses for long running queue-based processes, running on their behalf. I found an example here at this url that outlines how to use the OAuth Bearer tokens with signalr: http://blog.marcinbudny.com/search?q=Authentication+signalr+OAuth+#.V5KBaY52xTM. The problem is that his example seems to turn off the OAuthBearerTokens which I am using in my code for web api authentication and authorization. Here is the code from the article:
//app.UseCookieAuthentication(new CookieAuthenticationOptions());
//app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
//app.UseOAuthBearerTokens(OAuthOptions);
app.UseOAuthAuthorizationServer(OAuthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new ApplicationOAuthBearerAuthenticationProvider(),
});
and this is my code utilizing the technique in this article:
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
PublicClientId = "self";
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId, UserFactory),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(2),
AllowInsecureHttp = false
};
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerAuthenticationProvider()
});
var hubConfiguration = new HubConfiguration
{
Resolver = GlobalHost.DependencyResolver,
};
map.RunSignalR(hubConfiguration);
});
How can I allow signalr to access the user's information in the same way I would for web api when there are times I will need worker role processes to pass information to clients as well as client-side code does?