I am trying to secure my web api using azure active directory.
I have the following code to setup the web api:
public void BuildConfiguration(IAppBuilder appBuilder)
{
var HttpConfiguration = new System.Web.Http.HttpConfiguration();
HttpConfiguration.Filters.Add(new AuthorizeAttribute());
HttpConfiguration.MapHttpAttributeRoutes();
HttpConfiguration.Routes.MapHttpRoute("DefaultApi",
$"api/{{controller}}/{{action}}/{{id}}", new { id = RouteParameter.Optional });
var authenticationProvider = new OAuthBearerAuthenticationProvider()
{
OnValidateIdentity = AuthenticationOnValidateIdentity
};
var options = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = "https://xxx.onmicrosoft.com/yyy"
},
Tenant = "xxx.onmicrosoft.com",
Provider = authenticationProvider
};
appBuilder.UseWindowsAzureActiveDirectoryBearerAuthentication(options);
appBuilder.UseCors(CorsOptions.AllowAll);
appBuilder.UseWebApi(HttpConfiguration);
HttpConfiguration.EnsureInitialized();
}
Now, the code to perform the validation looks like this:
private async Task AuthenticationOnValidateIdentity(OAuthValidateIdentityContext context)
{
context.Ticket.Identity.AddClaim(new Claim("apikey", xx)); // <- what to put here instead of xx?
}
The client requests a token using this piece of code:
AuthenticationResult result = null;
try
{
result = await authContext.AcquireTokenAsync("https://xx.onmicrosoft.com/yyy", clientId, redirectUri, new PlatformParameters(PromptBehavior.Always), UserIdentifier.AnyUser);
}
catch (AdalException ex)
{
...
}
How can I pass additional info like an apikey (string) to the AAD validator so I can access the apikey in 'AuthenticationOnValidateIdentity'?
I tried the extraQueryParameters parameter like his:
result = await authContext.AcquireTokenAsync("https://xxx.onmicrosoft.com/yyy",
clientId,
redirectUri,
new PlatformParameters(PromptBehavior.Always),
UserIdentifier.AnyUser, "apikey=test");
but since I cannot get these values anywhere in AuthenticationOnValidateIdentity I wonder if that is the way to go.
So, the question remains: How can I pass aditional information to Azure Active Directory authentication flow?
Related
I am using aspnet core 5.0 webapi with CQRS in my project and already have jwt implementation. Not using role management from aspnet core but manually added for aspnet users table role field and it is using everywhere. In internet I can't find any article to implement keycloak for existing authentication and authorization. My point is for now users login with their email+password, idea is not for all but for some users which they already stored in keycloak, or for some users we will store there, give option login to our app using keycloak as well.
Scenario 1:
I have admin#gmail.com in both in my db and in keycloak and both are they in admin role, I need give access for both to login my app, first scenario already working needs implement 2nd scenarion beside first.
Found only this article which implements securing app (as we have already and not trying to replace but extend)
Medium keycloak
My jwt configuration looks like:
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services,
IConfiguration configuration)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
};
opt.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
return services;
}
My jwt service looks like:
public JwtGenerator(IConfiguration config)
{
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetSection("AppSettings:Token").Value));
}
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Role, user.Role.ToString("G").ToLower())
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
My login method looks like:
public async Task<GetToken> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null)
throw new BadRequestException("User not found");
UserManagement.ForbiddenForLoginUser(user);
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (result.Succeeded)
{
user.IsRoleChanged = false;
RefreshToken refreshToken = new RefreshToken
{
Name = _jwtGenerator.GenerateRefreshToken(),
DeviceName = $"{user.UserName}---{_jwtGenerator.GenerateRefreshToken()}",
User = user,
Expiration = DateTime.UtcNow.AddHours(4)
};
await _context.RefreshTokens.AddAsync(refreshToken, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return new GetToken(_jwtGenerator.CreateToken(user),refreshToken.Name);
}
throw new BadRequestException("Bad credentials");
}
My authorization handler:
public static IServiceCollection AddCustomMvc(this IServiceCollection services)
{
services.AddMvc(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
// Build the intermediate service provider
opt.Filters.Add<CustomAuthorizationAttribute>();
}).AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<CreateProjectCommand>());
return services;
}
What is best practise to implement keycloak authentiaction+authorization beside my current approach and give users to login with two scenarios, normal and keycloak login.
P.S. Ui is different and we are using angular this one just webapi for backend.
Since your login method returns a jwt, you could configure multiple bearer tokens by chaining .AddJwtBearer(), one for your normal login and one for keycloak.
Here is a link to a question that might solve your problem: Use multiple jwt bearer authentication.
Keycloak configuration:
Go to Roles -> Realm Roles and create a corresponding role.
Go to Clients -> Your client -> Mappers.
Create a new role mapper and select "User Realm Role" for Mapper Type, "roles" for Token Claim Name and "String" for Claim JSON Type. Without the mapping the role configured before would be nested somewhere else in the jwt.
You can use the debugger at jwt.io to check if your token is correct. The result should look like this:
{
"exp": 1627565901,
"iat": 1627564101,
"jti": "a99ccef1-afa9-4a62-965b-15e8d33de7de",
// [...]
// roles nested in realm_access :(
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
},
// [...]
// your mapped roles in your custom claim
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
// [...]
}
I am trying to create SAS policy for azure service bus namespace using shared access policy connectionstring in .NET Core 2.1.
I can create it using Microsoft.Azure.Management.ServiceBus NuGet package as follows
private static async Task<string> GetToken()
{
try
{
// Check to see if the token has expired before requesting one.
// We will go ahead and request a new one if we are within 2 minutes of the token expiring.
if (tokenExpiresAtUtc < DateTime.UtcNow.AddMinutes(-2))
{
Console.WriteLine("Renewing token...");
var tenantId = appOptions.TenantId;
var clientId = appOptions.ClientId;
var clientSecret = appOptions.ClientSecret;
var context = new AuthenticationContext($"https://login.microsoftonline.com/{tenantId}");
var result = await context.AcquireTokenAsync(
"https://management.core.windows.net/",
new ClientCredential(clientId, clientSecret)
);
// If the token isn't a valid string, throw an error.
if (string.IsNullOrEmpty(result.AccessToken))
{
throw new Exception("Token result is empty!");
}
tokenExpiresAtUtc = result.ExpiresOn.UtcDateTime;
tokenValue = result.AccessToken;
Console.WriteLine("Token renewed successfully.");
}
return tokenValue;
}
catch (Exception e)
{
Console.WriteLine("Could not get a new token...");
Console.WriteLine(e.Message);
throw e;
}
}
private static async Task CreateSASPolicy()
{
try
{
if (string.IsNullOrEmpty(namespaceName))
{
throw new Exception("Namespace name is empty!");
}
var token = await GetToken();
var creds = new TokenCredentials(token);
var sbClient = new ServiceBusManagementClient(creds)
{
SubscriptionId = appOptions.SubscriptionId,
};
List<AccessRights?> list = new List<AccessRights?> { AccessRights.Send };
var AuthRule = new SBAuthorizationRule { Rights = list };
var authorizationRuleName = "SendRule"; //policy name
Console.WriteLine("Creating SAS policy...");
var result = sbClient.Namespaces.CreateOrUpdateAuthorizationRuleAsync(resourceGroupName, namespaceName, authorizationRuleName, AuthRule).Result;
Console.WriteLine("Created SAS policy successfully.");
}
catch (Exception e)
{
Console.WriteLine("Could not create a SAS policy...");
Console.WriteLine(e.Message);
throw e;
}
}
But for above code I need to give at least "Azure Service Bus Data Owner" to the app which we are using to create token.
I have also tried using http client as follows
using (HttpClient httpclient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }))
{
httpclient.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(
System.Text.ASCIIEncoding.ASCII.GetBytes(
string.Format("{0}:{1}", "", accessToken))));
string baseAddress = #"https://management.core.windows.net/<subscriptionId>/services/ServiceBus/namespaces/<namespace>/AuthorizationRules/";
var sendRule = new SharedAccessAuthorizationRule("contosoSendAll",
new[] { AccessRights.Send });
var result = await httpclient.GetAsync(baseAddress).ConfigureAwait(false);
if (result.IsSuccessStatusCode)
{
var response = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
}
}
It returns 403(Forbidden).
Question :-
Is there any way to create SAS policy without giving "Azure Service Bus Data Owner" permission to app? How?
Can create SAS policy using shared access policy connectionstring? How?
Can create SAS policy by using current login user credentials? How?
Can create SAS policy using shared access policy connectionstring? How?
If you use shared access policy connectionstring to create SAS policy, we just can create policy for topic or subscription. For the namespace, we need to implement it with Azure Resource provider API. It the way you are currently using. For more details, please refer to here.
Regarding how to create policy fro the topic or queue, we can use the package Azure.Messaging.ServiceBus.Administration.
For example
ServiceBusAdministrationClient client = new ServiceBusAdministrationClient(connectionString);
QueueProperties queue =await client.GetQueueAsync("myqueue");
queue.AuthorizationRules.Add( new SharedAccessAuthorizationRule(
"manage",
new[] { AccessRights.Manage, AccessRights.Send, AccessRights.Listen })
);
queue= await client.UpdateQueueAsync(queue);
foreach (var rule in queue.AuthorizationRules) {
Console.WriteLine(rule.KeyName);
}
Is there any way to create SAS policy without giving "Azure Service Bus Data Owner" permission to app? How?
When we use Azure Resource Provider API to create the resource, we should have the right Azure RABC permissions. About creating policy, we need to have permissions Microsoft.ServiceBus/namespaces/authorizationRules/action and Microsoft.ServiceBus/namespaces/authorizationRules/write. If you do not want to use Azure Service Bus Data Owner, you can create a custom role with these permissions. For more details, please refer to here and here.
I'm trying to upgrade one of the projects I'm working on to use the Microsoft.Identity.Web nuget package. So far really working well but I'm having trouble figuring out how to add additional claims which I was previously doing by the following:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Events.OnAuthorizationCodeReceived = async ctx =>
{
var serviceProvider = services.BuildServiceProvider();
var distributedCache = serviceProvider.GetRequiredService<IDistributedCache>();
var identifier = ctx.Principal.FindFirst(ObjectIdentifierType)?.Value;
var cca = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(new ConfidentialClientApplicationOptions()
{
ClientId = "ClientId",
RedirectUri = "RedirectUri",
ClientSecret = "ClientSecret"
})
.WithAuthority(ctx.Options.Authority)
.Build();
var tokenCache = new SessionTokenCache(identifier, distributedCache);
tokenCache.Initialize(cca.UserTokenCache);
var token = await cca.AcquireTokenByAuthorizationCode(scopes, ctx.TokenEndpointRequest.Code).ExecuteAsync();
ctx.HandleCodeRedemption(token.AccessToken, token.IdToken);
// get the claims
var claimService = serviceProvider.GetRequiredService<ClaimService>();
var response = await apiClient.GetUserAdditionalClaimsAsync(token.AccessToken);
// add the claims
};
Now when I try to use the ITokenAcquisition.GetAccessTokenForUserAsync() method instead of using the ConfidentialClientApplicationBuilder
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftWebApp(options =>
{
Configuration.Bind("AzureAD", options);
options.Events.OnAuthorizationCodeReceived = async ctx =>
{
var serviceProvider = services.BuildServiceProvider();
var tokenAcquisition = serviceProvider.GetRequiredService<ITokenAcquisition>();
var token = await tokenAcquisition.GetAccessTokenForUserAsync("scopes");
// get the claims
var claimService = serviceProvider.GetRequiredService<ClaimService>();
var response = await apiClient.GetUserAdditionalClaimsAsync(token);
// add the claims
};
})
.AddMicrosoftWebAppCallsWebApi(Configuration, new[] { "scopes" })
.AddDistributedTokenCaches();
Any help would be so much appreciated on how best to handle this.
Thanks!
I get the following error:
You use the client credentials flow when using ConfidentialClientApplicationBuilder. I don't know why you use the ITokenAcquisition.GetAccessTokenForUserAsync() to instead.
You can use the below code sample for client credential flow :
// Even if this is a console application here, a daemon application is a confidential client application
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithTenantId("{tenantID}")
.WithClientSecret(config.ClientSecret)
.Build();
// With client credentials flows the scopes is ALWAYS of the shape "resource/.default", as the
// application permissions need to be set statically (in the portal or by PowerShell), and then granted by
// a tenant administrator
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
}
catch(MsalServiceException ex)
{
// Case when ex.Message contains:
// AADSTS70011 Invalid scope. The scope has to be of the form "https://resourceUrl/.default"
// Mitigation: change the scope to be as expected
}
Hello I have developed a Microsoft application using Microsoft Graph API in order to obtain planner data and store it in a database for now. On it's own the application works fine without any issue what so ever.
The next task for me is to integrate this separate application into the main company application. The main company's website uses form authentication. What is the best way to integrate this. Currently when I try to login to get authorized I am redirected to the form login not the Microsoft one
I have registered the application in the Microsoft application registration pool. I have also added the office 365 api
This is the token obtain code that i am using
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
tokenCache = new SessionTokenCache(
signedInUserID,
HttpContext.Current.GetOwinContext().Environment["System.Web.HttpContextBase"] as HttpContextBase);
//var cachedItems = tokenCache.ReadItems(appId); // see what's in the cache
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
tokenCache);
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes.Split(new char[] { ' ' }));
return result.Token;
}
// Unable to retrieve the access token silently.
catch (MsalSilentTokenAcquisitionException)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new Exception(Resource.Error_AuthChallengeNeeded);
}
}
This is the sign in method I am trying use when trying to directly log in
// Signal OWIN to send an authorization request to Azure.
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
I have solved this issue by implementing the following code
public ActionResult SignIn()
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
string redirectUri = Url.Action("Authorize", "Planner", null, Request.Url.Scheme);
Uri authUri = authContext.GetAuthorizationRequestURL("https://graph.microsoft.com/", SettingsHelper.ClientId,
new Uri(redirectUri), UserIdentifier.AnyUser, null);
// Redirect the browser to the Azure signin page
return Redirect(authUri.ToString());
}
public async Task<ActionResult> Authorize()
{
// Get the 'code' parameter from the Azure redirect
string authCode = Request.Params["code"];
AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority);
// The same url we specified in the auth code request
string redirectUri = Url.Action("Authorize", "Planner", null, Request.Url.Scheme);
// Use client ID and secret to establish app identity
ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);
try
{
// Get the token
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
authCode, new Uri(redirectUri), credential, SettingsHelper.O365UnifiedResource);
// Save the token in the session
Session["access_token"] = authResult.AccessToken;
return Redirect(Url.Action("Index", "Planner", null, Request.Url.Scheme));
}
catch (AdalException ex)
{
return Content(string.Format("ERROR retrieving token: {0}", ex.Message));
}
}
A link to the solution that helped tackle this was this. It's slightly old but still helped out massively
https://www.vrdmn.com/2015/05/using-office-365-unified-api-in-aspnet.html
I'm having an issue with the AcquireTokenSilentAsync method and was hoping anyone could help me out.
In the method below im trying to use the AcquireTokenSilentAsync method so I can use it later to make a call to the Microsoft graph api. Unfortunately the Users property of the ConfidentialClientApplication is empty, as a result of that the cca.AcquireTokenSilentAsync fails because it requires the first user in the parameter by calling cca.Users.First().
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
HttpContextWrapper httpContext = new HttpContextWrapper(HttpContext.Current);
TokenCache userTokenCache = new SessionTokenCache(signedInUserID, httpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
userTokenCache,
null);
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes.Split(new char[] { ' ' }), cca.Users.First());
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new ServiceException(
new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = Resource.Error_AuthChallengeNeeded,
});
}
}
}
I'm not sure why the ConfidentialClientApplication doesn't contain any Users, as the sign in at the start of the application works correctly. I am only using UseCookieAuthentication and UseOpenIdConnectAuthentication in the startup.
I hope anyone can help me with this problem!