I am working on an asp.net Web API and I have an web application that consumes this api.
Right now it is working perfectly since I don't have the [Authorize] part on my api controller.
So, if I want to secure this api, my web application will not be able anymore to fetch data from the API because it is not authorized.
So how can I send the token generated from my API to my web app and to allow it to fetch the needed data?
-I am using postman for testing my app;
-my api return jwt token;
-I am not really familiar with http headers.
My consuming web application controller :
public ActionResult Index()
{
IEnumerable<OperatorClass> OperatorObject = null;
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://localhost:44304/api/");
var ApiOpController = client.GetAsync("data");
client.DefaultRequestHeaders.Add("Authorization", "Bearer"+"");
ApiOpController.Wait();
var resultDisplay = ApiOpController.Result;
if (resultDisplay.IsSuccessStatusCode)
{
var readTable = resultDisplay.Content.ReadAsAsync<IList<OperatorClass>>();
readTable.Wait();
OperatorObject = readTable.Result;
}
else
{
OperatorObject = Enumerable.Empty<OperatorClass>();
ModelState.AddModelError(String.Empty, "No records found");
}
return View(OperatorObject);
}
My web API controller
[Authorize]
[HttpGet]
public IHttpActionResult GetOperators()
{
SchoolEntity myEntity = new SchoolEntity ();
IList<OperatorClass> OperatorObject = myEntity.Operator.Include("Operator").Select(x => new OperatorClass()
{
name = x.name,
lastname = x.lastname,
mobile = x.mobile,
username = x.username,
password = x.password
}).ToList<OperatorClass>();
return Ok(OperatorObject);
}
string token = <Your token>
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
this should work for you.
To be able to use your MVC controller, you need to store the token when it returns.
One way to do it is to store it using Session.
Assuming you are using sign in to get the token, anytime you sign in successfully you can store the token using the session. See below.
//For brevity after successful login
string myToken = <token returned from api>
HttpContext.Session.SetString("token", myToken);
//other codes
then you can use this in all of your controllers.
public async Task<ActionResult> Index()
{
IEnumerable<OperatorClass> OperatorObject = null;
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://localhost:44304/api/");
//note here
var token = HttpContext.Session.GetString("token");
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
//ApiOpController.Wait();
var resultDisplay = await client.GetAsync("data");
if (resultDisplay.IsSuccessStatusCode)
{
var readTable = await resultDisplay.Content.ReadAsAsync<IList<OperatorClass>>();
//readTable.Wait();
OperatorObject = readTable;
}
else
{
OperatorObject = Enumerable.Empty<OperatorClass>();
ModelState.AddModelError(String.Empty, "No records found");
}
return View(OperatorObject);
}
Related
I have a web api controller which retrieves data from my database and a mvc web app controller which consumes this web api.
In my web api I used Microsoft.Owin for security and generating a token. Later, I am using this token taken from my web api via postman, and placing it statically on my web app. What I want to do is dynamically store every token I generate from every request and not copy-pasting it every time.
I used this video for creating my web api and generatin jwt token, and this video to consume my web api. Please help me, I'm stuck here for days now.
EDIT:
My web api controller:
[Authorize]
[HttpGet]
public IHttpActionResult GetOperators()
{
IList<OperatorClass> OperatorObject = myEntity.Operator.Include("Operator").Select(x => new OperatorClass()
{ id = x.id,
name = x.name,
lastname = x.lastname,
mobile = x.mobile,
username = x.username,
password = x.password
}).ToList<OperatorClass>();
return Ok(OperatorObject);
}
Startup.cs
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
var myProvider = new MyAuthorizationServerProvider();
OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),
Provider = myProvider
};
MyAuthorizationServerProvider.cs
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
if(context.UserName=="test" && context.Password == "test")
{
context.Validated(identity);
}
else
{
context.SetError("Invalid grant", "Provided username or password are incorrect");
return;
}
}
My web application controller
if (opc.username == "test" && opc.password == "test")
{
string token = "07Jv8mQ-pg6MlGdAAVJqxzsJ";
IEnumerable<OperatorClass> OperatorObject = null;
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://localhost:44304/api/");
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
var resultDisplay = await client.GetAsync("data");
if (resultDisplay.IsSuccessStatusCode)
{
var readTable = resultDisplay.Content.ReadAsAsync<IList<OperatorClass>>();
readTable.Wait();
OperatorObject = readTable.Result;
return View(OperatorObject);
}
else
{
OperatorObject = Enumerable.Empty<OperatorClass>();
ModelState.AddModelError(String.Empty, "No records found");
ViewBag.Error = "Token error. It may be incorrect or it has already expired. Check your token provider!";
return View("Error");
}
}
else
{
ViewBag.Error = "Incorrect username or password!";
return View("Login");
}
1- Why don't you store that token in database tables or Session for later use in request.
2- Everytime you ask question try to share relevant code as well next time.
I want to create lets say a master/core api.Want to check for a certain parameter value and redirect to a an external api hosted in the same server.I have an api with uri http://hello.test.com/auth which takes two auth params Username and Password.Now i add a third parameter lets say Area.
{
"Username":"jason",
"Password":"bourne",
"Area":"mars"
}
Now coming to the master api, if with this uri for example http://master.test.com/v1/mster and i pass Username, Password and Area,and if the Area has value of lets say "mars" it should call the external mars api having uri http://mars.test.com/auth ,do the auth the process and return the response in the master api.is this possible?
With my /auth api i have this controller returning the response :
[HttpPost]
[Route(ApiEndpoint.AUTH)]
public HttpResponseMessage Auth(Login authBDTO)
{
if (!ModelState.IsValid)
return Request.CreateResponse(HttpStatusCode.BadRequest, ModelState);
using (AccountBusinessService accountService = new AccountBusinessService())
{
var result = accountService.Auth(authBDTO);
return Request.CreateResponse(HttpStatusCode.OK, result);
}
}
Any Help Appreciated.Couldnt find this exact scenario in here.Sorry if too naive.
Found a workaround.This did the work.
[Route(ApiEndpoint.SAS)]
public IHttpActionResult esp(Login auth)
{
if (auth.Coop == "PMC")
{
var httpWebRequest = (HttpWebRequest)WebRequest.Create("http://localhost:60069/api/v1/auth");
httpWebRequest.ContentType = "application/json";
httpWebRequest.Method = "POST";
using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
{
string json = new JavaScriptSerializer().Serialize(new
{
Username = auth.UserName,
Password = auth.Password
});
streamWriter.Write(json);
}
var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(result);
obj.BaseUrl = "http://localhost:60069/api/v1";
return Ok(obj);
}
}
I am quite new in web API implementation, I have created a web API service to use it with ASP.net web form applications as well as some stand alone applications(C# Console/Windows application) using HttpClient object.
I have implemented a basic JWT access token authentication with expiration time limit in web api, this authentication technique is working fine until token not expired, when token get expired web api does not accept request as token has expired! which is fine as per authentication implementation, but I want to implement refresh token logic in web api so token can renew/refersh and client should be able to use the web api resource.
I googled a lot but unable to find the proper implementation of refresh token logic. Please help me if any one has right approach to handle the expired access token.
Following are the steps that I have followed to use the web api in asp.net application.
In ASP.net web form login page I called the web API "TokenController" this controller take two arguments loginID and password and return the JWT token that I stored in session object.
Now whenever my client application need too use the web api resource has to send the access token in request header while making call to web api using httpclient.
But when token get expired client unable use the web api resource he has to login again and renew the token! this I don't want, user should not prompt to be login again as application session out time not elapsed yet.
How do I refresh the token without forcing user to login again.
If my given below JWT access token implementation logic is not suitable or it is incorrect, please let me know the correct way.
Following is the code.
WebAPI
AuthHandler.cs
public class AuthHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage errorResponse = null;
try
{
IEnumerable<string> authHeaderValues;
request.Headers.TryGetValues("Authorization", out authHeaderValues);
if (authHeaderValues == null)
return base.SendAsync(request, cancellationToken);
var requestToken = authHeaderValues.ElementAt(0);
var token = "";
if (requestToken.StartsWith("Bearer ", StringComparison.CurrentCultureIgnoreCase))
{
token = requestToken.Substring("Bearer ".Length);
}
var secret = "w$e$#*az";
ClaimsPrincipal cp = ValidateToken(token, secret, true);
Thread.CurrentPrincipal = cp;
if (HttpContext.Current != null)
{
Thread.CurrentPrincipal = cp;
HttpContext.Current.User = cp;
}
}
catch (SignatureVerificationException ex)
{
errorResponse = request.CreateErrorResponse(HttpStatusCode.Unauthorized, ex.Message);
}
catch (Exception ex)
{
errorResponse = request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message);
}
return errorResponse != null
? Task.FromResult(errorResponse)
: base.SendAsync(request, cancellationToken);
}
private static ClaimsPrincipal ValidateToken(string token, string secret, bool checkExpiration)
{
var jsonSerializer = new JavaScriptSerializer();
string payloadJson = string.Empty;
try
{
payloadJson = JsonWebToken.Decode(token, secret);
}
catch (Exception)
{
throw new SignatureVerificationException("Unauthorized access!");
}
var payloadData = jsonSerializer.Deserialize<Dictionary<string, object>>(payloadJson);
object exp;
if (payloadData != null && (checkExpiration && payloadData.TryGetValue("exp", out exp)))
{
var validTo = AuthFactory.FromUnixTime(long.Parse(exp.ToString()));
if (DateTime.Compare(validTo, DateTime.UtcNow) <= 0)
{
throw new SignatureVerificationException("Token is expired!");
}
}
var clmsIdentity = new ClaimsIdentity("Federation", ClaimTypes.Name, ClaimTypes.Role);
var claims = new List<Claim>();
if (payloadData != null)
foreach (var pair in payloadData)
{
var claimType = pair.Key;
var source = pair.Value as ArrayList;
if (source != null)
{
claims.AddRange(from object item in source
select new Claim(claimType, item.ToString(), ClaimValueTypes.String));
continue;
}
switch (pair.Key.ToUpper())
{
case "USERNAME":
claims.Add(new Claim(ClaimTypes.Name, pair.Value.ToString(), ClaimValueTypes.String));
break;
case "EMAILID":
claims.Add(new Claim(ClaimTypes.Email, pair.Value.ToString(), ClaimValueTypes.Email));
break;
case "USERID":
claims.Add(new Claim(ClaimTypes.UserData, pair.Value.ToString(), ClaimValueTypes.Integer));
break;
default:
claims.Add(new Claim(claimType, pair.Value.ToString(), ClaimValueTypes.String));
break;
}
}
clmsIdentity.AddClaims(claims);
ClaimsPrincipal cp = new ClaimsPrincipal(clmsIdentity);
return cp;
}
}
AuthFactory.cs
public static class AuthFactory
{
internal static DateTime FromUnixTime(double unixTime)
{
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
return epoch.AddSeconds(unixTime);
}
internal static string CreateToken(User user, string loginID, out double issuedAt, out double expiryAt)
{
var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
expiryAt = Math.Round((DateTime.UtcNow.AddMinutes(TokenLifeDuration) - unixEpoch).TotalSeconds);
issuedAt = Math.Round((DateTime.UtcNow - unixEpoch).TotalSeconds);
var payload = new Dictionary<string, object>
{
{enmUserIdentity.UserName.ToString(), user.Name},
{enmUserIdentity.EmailID.ToString(), user.Email},
{enmUserIdentity.UserID.ToString(), user.UserID},
{enmUserIdentity.LoginID.ToString(), loginID}
,{"iat", issuedAt}
,{"exp", expiryAt}
};
var secret = "w$e$#*az";
var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);
return token;
}
public static int TokenLifeDuration
{
get
{
int tokenLifeDuration = 20; // in minuets
return tokenLifeDuration;
}
}
internal static string CreateMasterToken(int userID, string loginID)
{
var payload = new Dictionary<string, object>
{
{enmUserIdentity.LoginID.ToString(), loginID},
{enmUserIdentity.UserID.ToString(), userID},
{"instanceid", DateTime.Now.ToFileTime()}
};
var secret = "w$e$#*az";
var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);
return token;
}
}
WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.MessageHandlers.Add(new AuthHandler());
}
}
TokenController .cs
public class TokenController : ApiController
{
[AllowAnonymous]
[Route("signin")]
[HttpPost]
public HttpResponseMessage Login(Login model)
{
HttpResponseMessage response = null;
DataTable dtblLogin = null;
double issuedAt;
double expiryAt;
if (ModelState.IsValid)
{
dtblLogin = LoginManager.GetUserLoginDetails(model.LoginID, model.Password, true);
if (dtblLogin == null || dtblLogin.Rows.Count == 0)
{
response = Request.CreateResponse(HttpStatusCode.NotFound);
}
else
{
User loggedInUser = new User();
loggedInUser.UserID = Convert.ToInt32(dtblLogin.Rows[0]["UserID"]);
loggedInUser.Email = Convert.ToString(dtblLogin.Rows[0]["UserEmailID"]);
loggedInUser.Name = Convert.ToString(dtblLogin.Rows[0]["LastName"]) + " " + Convert.ToString(dtblLogin.Rows[0]["FirstName"]);
string token = AuthFactory.CreateToken(loggedInUser, model.LoginID, out issuedAt, out expiryAt);
loggedInUser.Token = token;
response = Request.CreateResponse(loggedInUser);
}
}
else
{
response = Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
return response;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
}
PremiumCalculatorController.cs
PremiumCalculatorController : ApiController
{
[HttpPost]
public IHttpActionResult CalculatAnnualPremium(PremiumFactorInfo premiumFactDetails)
{
PremiumInfo result;
result = AnnualPremium.GetPremium(premiumFactDetails);
return Ok(result);
}
}
Web Form Application
Login.aspx.cs
public class Login
{
protected void imgbtnLogin_Click(object sender, System.EventArgs s)
{
UserInfo loggedinUser = LoginManager.ValidateUser(txtUserID.text.trim(), txtPassword.text);
if (loggedinUser != null)
{
byte[] password = LoginManager.EncryptPassword(txtPassword.text);
APIToken tokenInfo = ApiLoginManager.Login(txtUserID.text.trim(), password);
loggedinUser.AccessToken = tokenInfo.Token;
Session.Add("LoggedInUser", loggedinUser);
Response.Redirect("Home.aspx");
}
else
{
msg.Show("Logn ID or Password is invalid.");
}
}
}
ApiLoginManager.cs
public class ApiLoginManager
{
public UserDetails Login(string userName, byte[] password)
{
APIToken result = null;
UserLogin objLoginInfo;
string webAPIBaseURL = "http://localhost/polwebapiService/"
try
{
using (var client = new HttpClient())
{
result = new UserDetails();
client.BaseAddress = new Uri(webAPIBaseURL);
objLoginInfo = new UserLogin { LoginID = userName, Password = password };
var response = client.PostAsJsonAsync("api/token/Login", objLoginInfo);
if (response.Result.IsSuccessStatusCode)
{
string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
result = JsonConvert.DeserializeObject<APIToken>(jsonResponce);
}
response = null;
}
return result;
}
catch (Exception ex)
{
throw ex;
}
}
}
AnnualPremiumCalculator.aspx.cs
public class AnnualPremiumCalculator
{
protected void imgbtnCalculatePremium_Click(object sender, System.EventArgs s)
{
string token = ((UserInfo)Session["LoggedInUser"]).AccessToken;
PremiumFactors premiumFacts = CollectUserInputPremiumFactors();
PremiumInfo premiumDet = CalculatePremium(premiumFacts, token);
txtAnnulPremium.text = premiumDet.Premium;
//other details so on
}
public PremiumInfo CalculatePremium(PremiumFactors premiumFacts, string accessToken)
{
PremiumInfo result = null;
string webAPIBaseURL = "http://localhost/polwebapiService/";
try
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(webAPIBaseURL);
StringContent content = new StringContent(JsonConvert.SerializeObject(premiumFacts), Encoding.UTF8, "application/json");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = client.PostAsync("api/calculators/PremiumCalculator", content);
if (response.Result.IsSuccessStatusCode)
{
string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
result = JsonConvert.DeserializeObject<PremiumInfo>(jsonResponce);
}
response = null;
}
return result;
}
finally
{
}
}
}
above is a sample code to illustrate the issue, it may have some typo.
I have some remarks:
The access token is meant to be saved by the client and not in a session on the server. The same counts for the refresh token. The reason for that is, that there usually is no session. Smart clients can handle the token without session, MVC websites can use a cookie and the API doesn't know sessions. It is not forbidden, but then again you'll need to worry about session expiration and all users have to login again when you restart your server.
If you want to implement OAuth then read the specification. In there you will find everything you'll need to implement the refresh token.
In TokenController you handle the login. There you should check other conditions as well.
grant_type = password
Content-Type has to be "application/x-www-form-urlencoded"
the request should only be handled if send over a secured line (https).
When the access_token is obtained and only if the refresh_token is requested, you should include the refresh_token in the access_token.
You don't need a refresh token for client applications (grant_type = client_credentials) as those use a clientid / secret to obtain an access token. Extend TokenController to allow the client_credentials flow. Please note: refresh tokens are for users only and should be used only if they can be kept secret. A refresh token is very powerfull, so handle with care.
In order to refresh an access token you'll need to send the refresh token to the endpoint. In your case you can extend the TokenController to allow a refresh_token request. You'll need to check:
grant_type = refresh_token
Content-Type has to be "application/x-www-form-urlencoded"
There are several scenarios for the refresh token, which you can also combine:
Save the refresh token in a database. Each time a refresh token is used you can remove it from the database, then save the new refresh token which is also returned in the new access_token.
Set the refresh token to a longer lifetime and do not refresh it when the access token is refreshed. In this case the returned access_token does not include a new refresh token. That way you'll need to login again after the refresh_token expires.
Please note, a refresh token that never expires and cannot be revoked gives a user unlimited access, so be carefull with your implementation.
In my answer here you can see how a refresh token can be handled using Identity 2. You can consider to switch to Identity 2.
I think I've mentioned everything. Please let me know if I missed something or if something isn't clear.
This can be done with a separate persisting refresh token. A nice tutorial at http://www.c-sharpcorner.com/article/handle-refresh-token-using-asp-net-core-2-0-and-json-web-token/
I am using owin openid connect authentication where the authentication provider is hosted on a separate domain. The authentication process works nicely. I am able to view restricted pages upon successful login at the identity server.
But I want the external identity server to return back to "account/SignInCallback" controller action so that I can execute a few lines of code relevant for the member's account. In the browser's network activity it shows me "302 Found" for the "account/SignInCallback" but it doesn't hit the breakpoints attached to it. It directly goes to the request initiating url e.g. "account/Dashboard".
Is there an way I can force the system to return back to the specific url after login, even though requesting url was different?
public class AccountController : BaseController
{
public AccountController() : base()
{
}
[Authorize]
public ActionResult Dashboard()
{
return View();
}
[HttpPost]
[AllowAnonymous]
public ActionResult SignInCallback()
{
if (User.Identity.IsAuthenticated)
{
// Read claims and execute member specific codes
}
return View();
}
[AllowAnonymous]
public ActionResult Unauthorized()
{
return View();
}
}
The startup class is below:
public sealed class Startup
{
public void Configuration(IAppBuilder app)
{
string ClientCallbackUri = #"https://client.local/account/SignInCallback";
string IdServBaseUri = #"https://idm.website.com/core";
string TokenEndpoint = #"https://idm.website.com/core/connect/token";
string UserInfoEndpoint = #"https://idm.website.com/core/connect/userinfo";
string ClientId = #"WebPortalDemo";
string ClientSecret = #"aG90apW2+DbX1wVnwwLD+eu17g3vPRIg7p1OnzT14TE=";
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = IdServBaseUri,
RedirectUri = ClientCallbackUri,
PostLogoutRedirectUri = ClientUri,
ResponseType = "code id_token token",
Scope = "openid profile roles",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
TokenEndpoint,
ClientId,
ClientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(UserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
//id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
id.AddClaims(userInfoResponse.Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
}
}
});
}
}
It looks all you need is to set
n.AuthenticationTicket.Properties.RedirectUri = n.RedirectUri;
in your AuthorizationCodeReceived delegate
The individual auth templates do this by enabling AutomaticChallenge on the cookie middleware rather than the other auth middleware (OIDC in this case). Cookie redirects them to an AccountController login page, then they select the auth method, do the auth redirects, return to the account controller for the additional steps you want to add, and then they finish by redirecting back to the original page.
Here's a later version of that template for ASP.NET Core:
https://github.com/aspnet/Templates/blob/rel/1.0.5/src/Rules/StarterWeb/IndividualAuth/Controllers/AccountController.cs
https://github.com/aspnet/Templates/blob/rel/1.0.5/src/Rules/StarterWeb/IndividualAuth/Startup.cs
Note much of this is managed by the Identity framework, but it's not required.
Setup:
New MVC5 Project with just Web API. Added Facebook AppId and Secret.
I can get Token for my Web API from Token endpoint by passing in UserName and Password. Then use that token for further calls.
BUT
I want to register new users with the help of Facebook SDK in iOS app.
I am using Facebook SDK to get Access Token. (Assume at this point, I have an Access Token).
Next thing I know is to call api/Account/RegisterExternal endpoint by passing this token in Authorization header with Bearer [Access Token] but this result in 500 server error.
I guess I know the reason, Cookie is missing. I made the same call with a cookie from Fidler and it worked. (Cookie is received by going to URL provided by ExternalLogins endpoint).
As cookie is missing await Authentication.GetExternalLoginInfoAsync(); inside the RegisterExternal action returns null.
// POST api/Account/RegisterExternal
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("RegisterExternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
I don't want to make 3 calls to my Web API to ask for external logins and then goto that URL and authenticate in a Web Browser for Facebook access token and then call the RegisterExternal endpoint with that access token and Cookie that I need to collect between these calls.
As I said I didn't change anything in template except the Facebook Ids. Still the code is as below.
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static string PublicClientId { get; private set; }
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// 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
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
app.UseFacebookAuthentication(
appId: "xxxxxxxxxxxxxxx",
appSecret: "xxxxxxxxxxxxxxxxxxxxxxxx");
}
}
as far as I know, Web API doesn't need Cookie and that appears true when I have Local Token from Token endpoint but why does it require Cookie in the first place when doing ExternalRegister
WebApiConfig class looks like this and shouldn't config.SuppressDefaultHostAuthentication(); avoid any Cookie needs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
I don't know if I am missing the point here.. My intentions are to not need to use web browser in a native iOS app for the token. That is Facebook SDK to get access token and using that call RegisterExternal to get the Local Token and create that users Identity.
I did my homework and I am stuck on this thought.
Thoughts appreciated!
I was mistaken that it accepts the Social Token with cookie!
It doesn't accept any External Token directly.
The thing is.. MVC 5 is taking care of everything for us, i.e. collecting token from Social Medias and validating/processing it. After that it generates a local token.
The RegisterExternal method also requires cookies to be maintained, the solution does not.
I have written a blog post which will explain in detail. Added the straight forward answer below. I aimed to make it blend and feel integral part of Login/Signup flow of default MVC Web API to make sure its easy to understand.
After the below solution, Authorize attribute must be as below to work or you will get Unauthorized response.
[Authorize]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalBearer)]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ApplicationCookie)]
Use ExternalBearer if you want to allow only Tokens to use API, use ApplicationCookie if you want to allow only Logged cookie to use API i.e. from a website. User both if you want to allow the API for both.
Add this action to AccountController.cs
// POST api/Account/RegisterExternalToken
[OverrideAuthentication]
[AllowAnonymous]
[Route("RegisterExternalToken")]
public async Task<IHttpActionResult> RegisterExternalToken(RegisterExternalTokenBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
ExternalLoginData externalLogin = await ExternalLoginData.FromToken(model.Provider, model.Token);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.LoginProvider != model.Provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return InternalServerError();
}
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
bool hasRegistered = user != null;
ClaimsIdentity identity = null;
IdentityResult result;
if (hasRegistered)
{
identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
IEnumerable<Claim> claims = externalLogin.GetClaims();
identity.AddClaims(claims);
Authentication.SignIn(identity);
}
else
{
user = new ApplicationUser() { Id = Guid.NewGuid().ToString(), UserName = model.Email, Email = model.Email };
result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
var info = new ExternalLoginInfo()
{
DefaultUserName = model.Email,
Login = new UserLoginInfo(model.Provider, externalLogin.ProviderKey)
};
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
IEnumerable<Claim> claims = externalLogin.GetClaims();
identity.AddClaims(claims);
Authentication.SignIn(identity);
}
AuthenticationTicket ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
var currentUtc = new Microsoft.Owin.Infrastructure.SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromDays(365));
var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
Request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
// Create the response building a JSON object that mimics exactly the one issued by the default /Token endpoint
JObject token = new JObject(
new JProperty("userName", user.UserName),
new JProperty("id", user.Id),
new JProperty("access_token", accessToken),
new JProperty("token_type", "bearer"),
new JProperty("expires_in", TimeSpan.FromDays(365).TotalSeconds.ToString()),
new JProperty(".issued", currentUtc.ToString("ddd, dd MMM yyyy HH':'mm':'ss 'GMT'")),
new JProperty(".expires", currentUtc.Add(TimeSpan.FromDays(365)).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'"))
);
return Ok(token);
}
Add this helper method to ExternalLoginData class in helper region in AccountController.cs
public static async Task<ExternalLoginData> FromToken(string provider, string accessToken)
{
string verifyTokenEndPoint = "", verifyAppEndpoint = "";
if (provider == "Facebook")
{
verifyTokenEndPoint = string.Format("https://graph.facebook.com/me?access_token={0}", accessToken);
verifyAppEndpoint = string.Format("https://graph.facebook.com/app?access_token={0}", accessToken);
}
else if (provider == "Google")
{
return null; // not implemented yet
//verifyTokenEndPoint = string.Format("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={0}", accessToken);
}
else
{
return null;
}
HttpClient client = new HttpClient();
Uri uri = new Uri(verifyTokenEndPoint);
HttpResponseMessage response = await client.GetAsync(uri);
ClaimsIdentity identity = null;
if (response.IsSuccessStatusCode)
{
string content = await response.Content.ReadAsStringAsync();
dynamic iObj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);
uri = new Uri(verifyAppEndpoint);
response = await client.GetAsync(uri);
content = await response.Content.ReadAsStringAsync();
dynamic appObj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);
identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
if (provider == "Facebook")
{
if (appObj["id"] != Startup.facebookAuthOptions.AppId)
{
return null;
}
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, iObj["id"].ToString(), ClaimValueTypes.String, "Facebook", "Facebook"));
}
else if (provider == "Google")
{
//not implemented yet
}
}
if (identity == null)
return null;
Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer) || String.IsNullOrEmpty(providerKeyClaim.Value))
return null;
if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
return null;
return new ExternalLoginData
{
LoginProvider = providerKeyClaim.Issuer,
ProviderKey = providerKeyClaim.Value,
UserName = identity.FindFirstValue(ClaimTypes.Name)
};
}
and finally, the RegisterExternalTokenBindingModel being used by the action.
public class RegisterExternalTokenBindingModel
{
[Required]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[Display(Name = "Token")]
public string Token { get; set; }
[Required]
[Display(Name = "Provider")]
public string Provider { get; set; }
}
Yes, we pass the email along with Token details while registering, this will not cause you to change the code when using Twitter, as Twitter doesn't provide users email. We verify token comes from our app. Once email registered, hacked or somebody else's token cannot be used to change email or get a local token for that email as it will always return the local token for the actual user of the Social Token passed regardless of the email sent.
RegisterExternalToken endpoint works to get token in both ways i.e. register the user and send the Local token or if the user already registered then send the token.
Before everything, this is NOT A FULL Answer, this is just a note or an addition for the answer to avoid some problems which could cost you handful of days (in my case 3 days)
The previous answer is the full answer it just lacks from one thing, which is the following:
if you specified a role for the Authorize attribute, for example [Authorize("UserRole")] , the previous setup will still give you 401 error because the solution does not set the RoleClaim
and to solve this problem you have to add this line of code to the RegisterExternalToken method
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, "UserRole"));