Secure Web Api to consumed by console Appplicatuion [closed] - oauth-2.0

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
I have created one Asp core web api which will be consumed by C# console application outside the organization. This console application is scheduled to run periodically . So hits on Web api will come when console application run.
Please assist How Can i secure my Web Api to malware hit or unauthentic access. I can't use AD authentication as I am unable to register client application in AAD(Azure active directory) Please assist.

Generally speaking , there're lots ways to do that . For example , use a basic scheme authentication in which the client sends username:password with the base64-encoding . However . It's not that safe .
I suggest you use JWT token . The authentication of Jwt scheme is dead simple :
The client send a request to ask for a JWT token with client_id and client_key . (You might configure them in configuration file or database on server)
Tf the client_id and client_key matches , the Server send a response with a JWT access token , maybe an additional refresh token if you like ; otherwise , send a response with a 401.
The client consumes webapi with a Authorization: Bearer ${access_token} header. The server will decrypt the access_token and hit the correct action if valid.
Here's a how-to in details:
Dummy class to hold information
To represent the client_id and client_key sent by your console , Let's create a dummy Dto class :
public class AskForTokenRequest
{
public string ClientId { get; set; }
public string ClientKey { get; set; }
}
When creating and validating Jwt token , we need information about issuer , audience , and secret keys . To hold these information , let's create another dummy class :
public class SecurityInfo {
public static readonly string Issuer = "xxx";
public static readonly string[] Audiences = new[] { "yyy" };
public static readonly string SecretKey = "!##$%^&*()&!!!##$%^&*()&!!!##$%^&*()&!!!##$%^&*()&!!!##$%^&*()&!";
}
Before we move on , let's create a JwtTokenHelper to generate token :
The JwtTokenHelper helps to validate client_id & client_key and generate Jwt Token .
public class JwtTokenHelper
{
//private AppDbContext _dbContext { get; set; }
//public JwtTokenHelper(AppDbContext dbContext) {
// this._dbContext = dbContext;
//}
public virtual bool ValidateClient(string clientId, string clientKey)
{
// check the client_id & clientKey with database , config file , or sth else
if (clientId == "your_console_client_id" && clientKey == "your_console_client_key")
return true;
return false;
}
/// construct a token
public virtual JwtSecurityToken GenerateToken(string clientId, DateTime expiry, string audience)
{
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(clientId, "jwt"));
var token=new JwtSecurityToken
(
claims: identity.Claims,
issuer: SecurityInfo.Issuer,
audience: audience,
expires: expiry,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityInfo.SecretKey)),
SecurityAlgorithms.HmacSha256
)
);
return token;
}
public virtual string GenerateTokenString(string clientId, DateTime expiry,string audience)
{
// construct a jwt token
var token = GenerateToken(clientId,expiry,audience);
// convert the token to string
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
return tokenHandler.WriteToken(token);
}
}
Configure the server to enable JwtBearer authentication :
Add JwtTokenHelper to DI Container and Add authentication scheme of JwtBearer
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<JwtTokenHelper>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = SecurityInfo.Issuer,
ValidAudiences = SecurityInfo.Audiences,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = new List<SecurityKey> {
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityInfo.SecretKey) )
},
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(60)
};
});
services.AddMvc();
}
Don't forget to add app.UseAuthentication(); in your Configure() method .
How to use:
Now , Create a controller to generate Jwt token
[Route("/api/token")]
public class TokenController : Controller
{
private readonly JwtTokenHelper _tokenHelper;
public TokenController(JwtTokenHelper tokenHelper) {
this._tokenHelper = tokenHelper;
}
[HttpPost]
public IActionResult Create([FromBody] AskForTokenRequest client)
{
if(! this._tokenHelper.ValidateClient(client.ClientId , client.ClientKey))
return new StatusCodeResult(401);
DateTime expiry = DateTime.UtcNow.AddMinutes(60); // expires in 1 hour
var audience = "yyy";
var access_token = this._tokenHelper.GenerateTokenString(client.ClientKey, expiry,audience);
return new JsonResult(new {
access_token = access_token,
});
}
}
and protect you webapi with [Authorize] attribute :
public class HomeController : Controller
{
[Authorize]
public IActionResult GetYourWebApiMethod(){
return new ObjectResult(new {
Username = User.Identity.Name
});
}
}

Related

Authorization with .net core 3.1 with JWT Token response from API

Im new to C# and im struggling with authorization in ASP.Net Core 3.1 MVC web application.I know that there is a lot of instruction on google, i've been reading and watching for 3 days but can not work this out because every instruction i found, it use another way and im really confused.
The idea of my system is:
Step 1. I POST username and password to my API and it'll response with JWT Token (if account is correct)
Step 2. I decode the token and get the username, email, role for my website, set HttpClient header for another requests.
My problems:
How and where to set HttpClient header (with my token) only one time when user login
How to force users stay at the Login page if they aren't login yet
Here's my Login method
[HttpPost, AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel account)
{
string url = "accounts/signin";
var response = await new HttpClientHelper<LoginViewModel>().PostRequest(url, account);
var userToken = JsonConvert.DeserializeObject<UserToken>(response);
Console.Out.WriteLine(userToken.Token);
if (userToken.Token != null)
{
var token = new JwtSecurityToken(jwtEncodedString: userToken.Token);
var userId = token.Claims.First(c => c.Type == "userId").Value;
var username = token.Claims.First(c => c.Type == "unique_name").Value;
var role = token.Claims.First(c => c.Type == "role").Value;
HttpContext.Session.SetString("token", token.ToString());
HttpContext.Session.SetString("userId", userId);
HttpContext.Session.SetString("username", username);
HttpContext.Session.SetString("role", role);
return RedirectToAction("Home", "Index");
}
return RedirectToAction("Login", "Login");
}
My model to receive response from api
public class UserToken
{
public string Token { get; set; }
public string ValidFrom { get; set; }
public string ValidTo { get; set; }
}
FYI: Ive already recived the response from api and got the Token, but ive to set HttpClient header every time i make a request..
How and where to set HttpClient header (with my token) only one time when user login
As far as I know, we couldn't set the httpclient header only one time when user login. Normally, we could store the token into session or cookie and then read it from cookie or session when you want to send request to web api.
How to force users stay at the Login page if they aren't login yet
For this requirement, I suggest you could consider using the authentication middleware to achieve your requirement.
You could check the user's session inside this middleware, if this user doesn't contains the session then you could modify the request path to login page.
More details, you could refer to below example:
//Below cods should add after app.usesession in startup.cs Configure method
app.Use((context, next) =>
{
string token = context.Session.GetString("token");
if (token == null)
{
context.Request.Path = "/account/login";
}
return next.Invoke();
});

Azure AD B2C - Not redirecting to SignIn/SignUp Page(login.microsoftonline.com...)

I have a project that I have started building, and want to make us of Azure AD B2C - I have followed some tutorials and tested by creating a new MVC app from scratch and I got it working, however, when I try implement it into my existing project, then it does not redirect to the SignIn/SignUp page(this is the login.microsoftonline.com...) url. I know my code works to redirect to this url as it worked on the new project I created to test, so just not sure why it wont on my existing project.
This is in my Web.Config:
<add key="ida:Tenant" value="Name.onmicrosoft.com" />
<add key="ida:ClientId" value="GUID" />
<add key="ida:ClientSecret" value="Secret" />
<add key="ida:AadInstance" value="https://login.microsoftonline.com/tfp/{0}/{1}/v2.0/.well-known/openid-configuration" />
<add key="ida:RedirectUri" value="https://localhost:44382/" />
<add key="ida:SignUpSignInPolicyId" value="B2C_1_SiUpIn" />
<add key="ida:EditProfilePolicyId" value="B2C_1_SiPe" />
<add key="ida:ResetPasswordPolicyId" value="B2C_1_SSPR" />
ActionLink:
#Html.ActionLink("Sign up / Sign in", "SignUpSignIn", "Account", routeValues: null, htmlAttributes: new { id = "signUpSignInLink" })
This is the SignUpSignIn function I am calling:
[AllowAnonymous]
public void SignUpSignIn()
{
// Use the default policy to process the sign up / sign in flow
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge();
return;
}
Response.Redirect("/");
}
Below is the code from my Startup:
public partial class Startup
{
// App config settings
public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
public static string ServiceUrl = ConfigurationManager.AppSettings["api:TaskServiceUrl"];
// B2C policy identifiers
public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
public static string DefaultPolicy = SignUpSignInPolicyId;
// API Scopes
public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope };
// OWIN auth middleware constants
public const string ObjectIdElement = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
// Authorities
public static string Authority = String.Format(AadInstance, Tenant, DefaultPolicy);
/*
* Configure the OWIN middleware
*/
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
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,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// 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 = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}"
}
);
}
/*
* On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
* If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
*/
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
{
notification.ProtocolMessage.Scope = OpenIdConnectScopes.OpenId;
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseTypes.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
/*
* Catch any failures received by the authentication middleware and handle appropriately
*/
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
notification.Response.Redirect("/Account/ResetPassword");
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
/*
* Callback function when an authorization code is received
*/
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);
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, Scopes);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
}
When I click on this ActionLink it hits the controller but then does not redirect, it just returns this URL:
https://localhost:44382/account/login?ReturnUrl=%2faccount%2fsignupsignin
One thing I must mention, is I purchased a template that I am using - Dont know if this might have any effect - I am completely stumped and dont know what else I can look at....
If you need anything that I have not posted, please let me know.
If anyone is able to assist me in the right direction, I would greatly appreciate it.
Thanks!
So unfortunately I didnt find a specific solution to FIX the problem, as I could not pin point what exactly the problem was. However, I did sort out the problem by creating a brand new project and moved the items from the template I bought(which was an mvc project) to the newly created project. Pain in the butt as I needed to fix any bugs that occurred because of this, but it ended up working. There was obviously something in the bought template that was causing the issue.
So just to give some context - The template I bought had different frameworks you could use(MVC, PHP, Angular etc.), and I used the MVC project that contained the template and I just carried on building on that MVC project, so I am assuming there was something within that project that was causing the problem.

Ws-Federation Authentication in MVC not keeping Claims information after SAML2.0 verification

I'm trying to add a new auth method with Azure ACS to support users from an ADFS but I'm having a very specific issue.
I'm able to validate the SAML2.0 with the following config:
var audienceRestriction = new AudienceRestriction(AudienceUriMode.Never);
var issuerRegistry = new ConfigurationBasedIssuerNameRegistry();
issuerRegistry.AddTrustedIssuer("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "https://XXXX.accesscontrol.windows.net/");
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions
{
MetadataAddress = "https://XXXXX.accesscontrol.windows.net/federationmetadata/2007-06/federationmetadata.xml",
Wtrealm = "http://someurl/",
SecurityTokenHandlers = new SecurityTokenHandlerCollection
{
new EncryptedSecurityTokenHandlerEx(new X509CertificateStoreTokenResolver(StoreName.My,StoreLocation.LocalMachine)),
new SamlSecurityTokenHandlerEx
{
CertificateValidator = X509CertificateValidator.None,
Configuration = new SecurityTokenHandlerConfiguration()
{
IssuerNameRegistry = issuerRegistry,
AudienceRestriction = audienceRestriction
}
}
},
});
With the handler implemented like this:
public class SamlSecurityTokenHandlerEx : Saml2SecurityTokenHandler, ISecurityTokenValidator
{
public override bool CanReadToken(string securityToken)
{
return base.CanReadToken(XmlReader.Create(new StringReader(securityToken)));
}
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters,
out SecurityToken validatedToken)
{
validatedToken = ReadToken(new XmlTextReader(new StringReader(securityToken)), Configuration.ServiceTokenResolver);
var claims = new ClaimsPrincipal(ValidateToken(validatedToken));
return claims;
}
public int MaximumTokenSizeInBytes { get; set; }
}
If I inspect the claims in the ValidateToken it is authenticated and with the claims I want but after it calls the callback page (where I want to create a new proper login for the webapp) It no longer has any information about the Federated auth.
Solved!
I was launching the ACS page from the same mechanism as the other external auth providers but for some reason it was failing. Calling the ACS login page (https://someacs.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm=https%3a%2f%2fsomeappsite%2f) directly solved my issue.

Authorize an Action using URL parameters

I have a MVC 5 application that has controllers dressed with the [Authorize] attribute. However, requirements state that clients may not be able to login to the web application (as this web app will be deployed on signage players -- not web browsers -- that accepts URLs as input to access specific actions in the web app).
In response, I was thinking about using a URL that a client could use that would authorize their actions, in place of logging in...in the format of:
http://{website}/Action?token={/* randomly generated key for this user */}
How would I go about implementing this without changing my current code dressed with the [Authorize] attribute?
I did change my code slightly. Instead of dressing my controllers with [Authorize], I created a custom authorize attribute called [TokenAuthorize].
I created a SecurityManager class to generate and validate a token. This was adapted from Primary Objects, although I chose not to use any of their client-side code or hash using ip or userAgent. Also, I created a TokenIdentity class to create an Identity for authorizing an action. This was adapted from Steve's Coding Blog. Jelle's link also helped in authorizing a user by token using GenericPrincipal: HttpContext.Current.User = new GenericPrincipal(new TokenIdentity(username), roles.ToArray());
After implementing this, I was able to authorize an action using a url parameter called token: http://{website}/Action?token={/* randomly generated key for this user */}
TokenAuthorize.cs
public class TokenAuthorize : AuthorizeAttribute
{
private const string SecurityToken = "token";
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (Authorize(filterContext))
{
return;
}
HandleUnauthorizedRequest(filterContext);
}
private bool Authorize(AuthorizationContext actionContext)
{
try
{
HttpContextBase context = actionContext.RequestContext.HttpContext;
string token = context.Request.Params[SecurityToken];
// check if the token is valid. if so, authorize action.
bool isTokenAuthorized = SecurityManager.IsTokenValid(token);
if (isTokenAuthorized) return true;
// if the token is not valid, check if the user is authorized by default.
bool isDefaultAuthorized = AuthorizeCore(context);
return isDefaultAuthorized;
}
catch (Exception)
{
return false;
}
}
}
SecurityManager.cs (adapted from Primary Objects)
public class SecurityManager
{
private const string Alg = "HmacSHA256";
private const string Salt = "rz8LuOtFBXphj9WQfvFh";
// Generates a token to be used in API calls.
// The token is generated by hashing a message with a key, using HMAC SHA256.
// The message is: username
// The key is: password:salt
public static string GenerateToken(string username, string password)
{
string hash = string.Join(":", new string[] { username });
string hashLeft;
string hashRight;
using (HMAC hmac = HMAC.Create(Alg))
{
hmac.Key = Encoding.UTF8.GetBytes(GetHashedPassword(password));
hmac.ComputeHash(Encoding.UTF8.GetBytes(hash));
hashLeft = Convert.ToBase64String(hmac.Hash);
hashRight = string.Join(":", username);
}
return Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Join(":", hashLeft, hashRight)));
}
// used in generating a token
private static string GetHashedPassword(string password)
{
string key = string.Join(":", new string[] { password, Salt });
using (HMAC hmac = HMAC.Create(Alg))
{
// Hash the key.
hmac.Key = Encoding.UTF8.GetBytes(Salt);
hmac.ComputeHash(Encoding.UTF8.GetBytes(key));
return Convert.ToBase64String(hmac.Hash);
}
}
// Checks if a token is valid.
public static bool IsTokenValid(string token)
{
var context = new ApplicationDbContext();
try
{
// Base64 decode the string, obtaining the token:username.
string key = Encoding.UTF8.GetString(Convert.FromBase64String(token));
// Split the parts.
string[] parts = key.Split(':');
if (parts.Length != 2) return false;
// Get the username.
string username = parts[1];
// Get the token for said user
// var computedToken = ...
// Compare the computed token with the one supplied and ensure they match.
var result = (token == computedToken);
if (!result) return false;
// get roles for user (ASP.NET Identity 2.0)
var user = context.Users.Single(u => u.UserName == username);
var rolesPerUser = context.UserRoles.Where(x => x.UserId == user.Id).ToList();
var roles = rolesPerUser.Select(role => context.Roles.Single(r => r.Id == role.RoleId).Name);
// NOTE: define public DbSet<IdentityUserRole> UserRoles { get; set; } ...
// ... in your DbContext in IdentityModels.cs
HttpContext.Current.User = new GenericPrincipal(new TokenIdentity(username), roles.ToArray());
return true;
}
catch
{
return false;
}
}
}
TokenIdentity.cs (adapted from Steve's Coding Blog)
public class TokenIdentity : IIdentity
{
public string User { get; private set; }
public TokenIdentity(string user)
{
this.User = user;
}
public string Name => User;
public string AuthenticationType => "ApplicationToken";
public bool IsAuthenticated => true;
}

How do I convert a refresh token to an access token using the LiveConnect API (C#)

I'm trying to create a LiveConnectClient with only a refresh token that was provided to me via asp.net identity (using OWIN) and the ProviderKey. It looks like the only way to do this without needing HttpContextBase is via InitializeSessionAsync.
When I try and create the client I'm getting:
Microsoft.Live.LiveAuthException: The user ID from the given RefreshTokenInfo instance does not match the refresh token.
Not really sure what user ID it is expecting as I'm giving it the provider key that was passed via ASP.NET Identity (17 chars in my case). Below is my code.
public class Class1
{
protected async Task<LiveConnectClient> GetLiveConnectClient()
{
var authClient = new LiveAuthClient(_clientId, _clientSecret, null, new RefreshTokenHandler(_refreshToken, _providerKey));
var session = await authClient.InitializeSessionAsync("http://.../signin-microsoft");
return new LiveConnectClient(session.Session);
}
}
public class RefreshTokenHandler : IRefreshTokenHandler
{
private readonly string _refreshToken;
private readonly string _userId;
public RefreshTokenHandler(string refreshToken, string userId)
{
_refreshToken = refreshToken;
_userId = userId;
}
public Task<RefreshTokenInfo> RetrieveRefreshTokenAsync()
{
return Task.FromResult(new RefreshTokenInfo(_refreshToken, _userId));
}
public async Task SaveRefreshTokenAsync(RefreshTokenInfo tokenInfo)
{
await Task.Delay(0);
}
}

Resources