I have a very thin front end ASP.NET MVC 5 application that talks to a WebApi 2 back end. These are separate applications.
I have gotten the authentication token from the WebApi. I have to get it at the time that the user logs in. I store it in session state, but that is obviously the wrong place. I have situation where the user is still logged in but the auth token is no longer in session.
I need to store it along side my authentication cookie, and it needs to have the same lifespan. Why is there not an way to do this out of the box? This is a situation that thousands of programmers face, I am sure.
Here is the code where I am storing it into Session:
/// <summary>
/// Configure the application sign-in manager which is used in this application.
/// </summary>
public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
public override async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
{
var status = await base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout);
if (status == SignInStatus.Success)
await PasswordSaveTokenAsync(userName, password);
return status;
}
/// <summary>
/// Get the token from the Web API with the given user name (<paramref name="userName"/>) and password
/// (<paramref name="password"/>) and save it to the session state.
/// </summary>
/// <param name="userName">User name.</param>
/// <param name="password">Password.</param>
private async Task PasswordSaveTokenAsync(string userName, string password)
{
var baseAddress = Config.WebApiAddress;
var client = new HttpClient { BaseAddress = baseAddress };
var response = await client.PostAsync("Token", new StringContent(String.Format("grant_type=password&username={0}&password={1}", userName, password), Encoding.UTF8));
response.EnsureSuccessStatusCode();
var tokenResponse = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(tokenResponse);
var token = json["access_token"].ToString();
Session.AccessToken = token;
}
}
Here is the solution that I cam up with.
In Global.asax:
public class MvcApplication : System.Web.HttpApplication
{
// Other members removed for brevity.
protected void Session_Start()
{
var cookie = Request.Cookies["AccessToken"];
if (cookie != null && cookie.Value != null)
Session["AccessToken"] = cookie.Value;
}
}
In ApplicationSignInManager.cs:
public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
// Comments and other members removed for brevity.
public override async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
{
var status = await base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout);
if (status == SignInStatus.Success)
await PasswordSaveTokenAsync(userName, password);
return status;
}
private async Task PasswordSaveTokenAsync(string userName, string password)
{
var baseAddress = Config.WebApiAddress;
var client = new HttpClient { BaseAddress = baseAddress };
var response = await client.PostAsync("Token", new StringContent(String.Format("grant_type=password&username={0}&password={1}", userName, password), Encoding.UTF8));
response.EnsureSuccessStatusCode();
var tokenResponse = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(tokenResponse);
var token = json["access_token"].ToString();
var expires = DateTime.Parse(json[".expires"].ToString());
HttpContext.Current.Response.Cookies.Add(new HttpCookie("AccessToken")
{
Value = token,
HttpOnly = true,
Expires = expires,
});
HttpContext.Current.Session["AccessToken"] = token;
}
}
You could put it in a cookie also.
Be sure to mark the cookie as http only
Related
I have implemented the Sign-In-With-Apple with Firebase. And I also have the functionality to delete a user. This is what I do:
static Future<bool> deleteUser(BuildContext context) async {
try {
await BackendService().deleteUser(
context,
);
await currentUser!.delete(); // <-- this actually deleting the user from Auth
Provider.of<DataProvider>(context, listen: false).reset();
return true;
} on FirebaseException catch (error) {
print(error.message);
AlertService.showSnackBar(
title: 'Fehler',
description: error.message ?? 'Unbekannter Fehler',
isSuccess: false,
);
return false;
}
}
As you can see I delete all the users data and finally the user himself from auth.
But Apple still thinks I am using the App. I can see it inside my Settings:
Also when trying to sign in again with apple, it acts like I already have an account. But I just deleted it and there is nothing inside Firebase that says that I still have that account?
How can I completely delete an Apple user from Firebase? What am I missing here?
Apple and some other 3rd party identity provider do not provide APIs to do so commonly.
Access to those data may lead to privacy issue, for e.g., a malicious app can remove the authorization information after access to user profile.
But if you want to do a "graceful" logout, you can ask your users to logout from iOS Settings, and listen to the server-to-server notification for revoking.
Although users account has been deleted on firebase it has not been removed from Apple's system. At the time of writing firebase SDK for Apple is still working on this feature git hub issue (Planned for Q4 2022 or Q1 2023), as flutter and react native are probably dependant on base SDK a custom implementation is needed until this is available.
According to Apple, to completely remove users Apple account you should obtain Apple's refresh token using generate_tokens API and then revoke it using revoke_tokens API.
High level description:
Client side (app): Obtain Apple authorization code.
Send authorization code to your server.
Server side: Use Apples p8 secret key to create jwt token. Jwt token will be used for authenticating requests towards Apple's API
Server side: Trade authorization code for refresh_token (see first link above)
Server side: Revoke refresh_token (see second link above)
Detailed description:
https://stackoverflow.com/a/72656672/6357154
.NET implantation of the server side process.
Assumptions:
_client is a HttpClient registered in DI contrainer with base url from Apple docs posted above
AppleClientOptions contains the same values used for Apple setup on firebase.
/// <summary>
/// Gets apple refresh token
/// SEE MORE: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
/// </summary>
/// <param name="jwtToken"></param>
/// <param name="authorizationCode"></param>
/// <returns></returns>
public async Task<string> GetTokenFromApple(string jwtToken, string authorizationCode)
{
IEnumerable<KeyValuePair<string, string>> content = new[]
{
new KeyValuePair<string, string>("client_id", _appleClientOptions.ClientId),
new KeyValuePair<string, string>("client_secret", jwtToken),
new KeyValuePair<string, string>("code", authorizationCode),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
};
var encodedContent = new FormUrlEncodedContent(content);
var response = await _client.PostAsync("auth/token", encodedContent);
var responseAsString = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var appleTokenResponse = JsonConvert.DeserializeObject<AppleTokenResponse>(responseAsString);
return appleTokenResponse.refresh_token;
}
_logger.LogError($"GetTokenFromApple failed: {responseAsString}");
return null;
}
/// <summary>
/// Revokes apple refresh token
/// SEE MORE: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
/// </summary>
/// <param name="jwtToken"></param>
/// <param name="refreshToken"></param>
/// <returns></returns>
public async Task<bool> RevokeToken(string jwtToken, string refreshToken)
{
IEnumerable<KeyValuePair<string, string>> content = new[]
{
new KeyValuePair<string, string>("client_id", _appleClientOptions.ClientId),
new KeyValuePair<string, string>("client_secret", jwtToken),
new KeyValuePair<string, string>("token", refreshToken),
new KeyValuePair<string, string>("token_type_hint", "refresh_token"),
};
var response = await _client.PostAsync("auth/revoke", new FormUrlEncodedContent(content));
return response.IsSuccessStatusCode;
}
private string GenerateAppleJwtTokenLinux()
{
var epochNow = (int) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
var (payload, extraHeaders) = CreateJwtPayload(
epochNow,
_appleClientOptions.TeamId,
_appleClientOptions.ClientId,
_appleClientOptions.KeyId);
var privateKeyCleaned = Base64Decode(_appleClientOptions.PrivateKey)
.Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
.Replace("-----END PRIVATE KEY-----", string.Empty)
.Replace("\r\n", string.Empty)
.Replace("\r\n", string.Empty);
var bytes = Convert.FromBase64String(privateKeyCleaned);
using var ecDsaKey = ECDsa.Create();
ecDsaKey!.ImportPkcs8PrivateKey(bytes, out _);
return Jose.JWT.Encode(payload, ecDsaKey, JwsAlgorithm.ES256, extraHeaders);
}
private static (Dictionary<string, object> payload, Dictionary<string, object> extraHeaders) CreateJwtPayload(
int epochNow,
string teamId,
string clientId,
string keyId)
{
var payload = new Dictionary<string, object>
{
{"iss", teamId},
{"iat", epochNow},
{"exp", epochNow + 12000},
{"aud", "https://appleid.apple.com"},
{"sub", clientId}
};
var extraHeaders = new Dictionary<string, object>
{
{"kid", keyId},
{"alg", "ES256"}
};
return (payload, extraHeaders);
}
/// <summary>
/// https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse
/// </summary>
public class AppleTokenResponse
{
public string access_token { get; set; }
public string expires_in { get; set; }
public string id_token { get; set; }
public string refresh_token { get; set; }
public string token_type { get; set; }
}
public class AppleClientOptions
{
public string TeamId { get; set; }
public string ClientId { get; set; }
public string KeyId { get; set; }
public string PrivateKey { get; set; }
}
public async Task<bool> DeleteUsersAccountAsync(string appleAuthorizationCode)
{
// Get jwt token:
var jwtToken = _appleClient.GenerateAppleJwtTokenLinux(); // Apple client is code form above, registered in DI.
// Get refresh token from authorization code:
var refreshToken = await _appleClient.GetTokenFromApple(jwtToken, appleAuthorizationCode);
if (string.IsNullOrEmpty(refreshToken)) return false;
// Delete token:
var isRevoked = await _appleClient.RevokeToken(jwtToken, refreshToken);
_logger.LogInformation("Deleted apple tokens for {UserId}", userId);
if (!isRevoked) return false;
return true;
}
Other implementation examples:
https://github.com/jooyoungho/apple-token-revoke-in-firebase
https://github.com/invertase/react-native-apple-authentication/issues/282
You did actually delete the user from Firebase but Apple doesn't know about that. You should delete that information also from Apple. Open the Settings app on your iPhone, then tap on your name at the top. Then press "Password & Security", then "Apple ID logins". All Apple ID logins should be listed there and can be deleted.
so... Apple does not provide this service. But I found a workaround.
My sign in process:
1. Check if user signed in before
// Create an `OAuthCredential` from the credential returned by Apple.
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);
// If you can not access the email property in credential,
// means that user already signed in with his appleId in the application once before
bool isAlreadyRegistered = appleCredential.email == null;
Now to the crucial part:
2. sign in user and check if that uid already exists in Firebase
final UserCredential result =
await FirebaseAuth.instance.signInWithCredential(
oauthCredential,
);
isAlreadyRegistered = await BackendService.checkIfUserIdExists(
result.user?.uid ?? '',
);
checkIfUserIdExists is quite simple as well:
static Future<bool> checkIfUserIdExists(String userId) async {
try {
var collectionRef = FirebaseFirestore.instance.collection(
BackendKeys.users,
);
var doc = await collectionRef.doc(userId).get();
return doc.exists;
} on FirebaseException catch (e) {
return false;
}
}
I created both a MVC 5 web app hosted on Azure and a WPF client. My short term purpose (as if I can achieve that I'll be able to implement all my uses case) is the following:
Enforce Azure Ad authentification on the WPF client
Have the MVC web app to check through Azure Graph API the AD group membership of the user authentified in the client
Send back Graph API object to the client (IUser, Group...)
Use group membership to define Authorization on controllers
My actual issue is the following:
The user launch the app, and is prompted for authentication. I guess it work as I can display the user's mail and I have an access token.
The user tries to access a web api controller and it works fine
The user tries to access another web api controller decorated with [Authorize] and i get in return some HTML page stating this : "We can't sign you in.Your browser is currently set to block JavaScript. You need to allow JavaScript to use this service."
From what I've found searching on the web it seems that it could be related to my web app that is not configured properly (I already tried to add my webapp url in trusted sites and I'm sure that my controller URL is Ok). i cannot find much doc on native client + AAD + MVC so I don't really know how to correct it.
Here's my startup.auth.cs from the webapp :
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:AppKey"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
private static string certName = ConfigurationManager.AppSettings["ida:CertName"];
public static readonly string Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
string graphResourceId = ConfigurationManager.AppSettings["ida:GraphUrl"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
//
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
//
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
#region Certs (not used)
if (certName.Length != 0)
{
// Create a Client Credential Using a Certificate
//
// Initialize the Certificate Credential to be used by ADAL.
// First find the matching certificate in the cert store.
//
X509Certificate2 cert = null;
X509Store store = new X509Store(StoreLocation.CurrentUser);
try
{
store.Open(OpenFlags.ReadOnly);
// Place all certificates in an X509Certificate2Collection object.
X509Certificate2Collection certCollection = store.Certificates;
// Find unexpired certificates.
X509Certificate2Collection currentCerts = certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);
// From the collection of unexpired certificates, find the ones with the correct name.
X509Certificate2Collection signingCert = currentCerts.Find(X509FindType.FindBySubjectDistinguishedName, certName, false);
if (signingCert.Count == 0)
{
// No matching certificate found.
return Task.FromResult(0);
}
// Return the first certificate in the collection, has the right name and is current.
cert = signingCert[0];
}
finally
{
store.Close();
}
// Then create the certificate credential.
ClientAssertionCertificate credential = new ClientAssertionCertificate(clientId, cert);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst(
"http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
AuthenticationHelper.token = result.AccessToken;
}
#endregion
else
{
// Create a Client Credential Using an Application Key
ClientCredential credential = new ClientCredential(clientId, appKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
AuthenticationHelper.token = result.AccessToken;
}
return Task.FromResult(0);
}
}
});
}
}
Here's the controller which can be acceded when not decorated with [Authorize] but in that case the action throw a null exception (but if I can't get it fixed i'll post another question):
[System.Web.Http.Authorize]
public class UserADGraphController : ApiController
{
[ResponseType(typeof(IUser))]
[System.Web.Http.Route("api/UserADGraphController/GetMyInformations")]
public IHttpActionResult GetMyInformations()
{
try
{
string uID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
if (uID == null)
return Ok("UId null");
ActiveDirectoryClient client = AuthenticationHelper.GetActiveDirectoryClient();
if (client == null)
return Ok("Client null");
IUser adUser = client.Users.Where(u => u.ObjectId == uID).ExecuteAsync().Result.CurrentPage.SingleOrDefault();
if (adUser == null)
{
return NotFound();
}
return Ok(adUser);
}
catch (Exception e)
{
return Ok(e.Message + " " + e.StackTrace);
}
and finally here are relevant parts of the client:
In the mainviewmodel class:
#region Azure AD auth properties
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
Uri redirectUri = new Uri(ConfigurationManager.AppSettings["ida:RedirectUri"]);
private static string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
private static string AppServiceResourceId = ConfigurationManager.AppSettings["todo:AppServiceResourceId"];
private static string AppServiceBaseAddress = ConfigurationManager.AppSettings["todo:AppServiceBaseAddress"];
private HttpClient httpClient;
private AuthenticationContext authContext = null;
#endregion
In the mainviewmodel constructor:
authContext = new AuthenticationContext(authority);
httpClient = new HttpClient();
My sign in method:
{
AuthenticationResult result = null;
try
{
result = authContext.AcquireToken(AppServiceResourceId, clientId, redirectUri, PromptBehavior.Auto);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
SignInLabelContent = "Connected to azure AD as " + result.UserInfo.DisplayableId;
}
catch (AdalException ex)
{
if (ex.ErrorCode == "user_interaction_required")
{
}
else
{
// An unexpected error occurred.
string message = ex.Message;
if (ex.InnerException != null)
{
message += "Inner Exception : " + ex.InnerException.Message;
}
Messenger.Default.Send<NotificationMessage>(new NotificationMessage(message));
//MessageBox.Show(message);
}
return;
}
}
The method that access the protected controller:
IUser me = null;
AuthenticationResult result = null;
result = authContext.AcquireToken(AppServiceResourceId, clientId, redirectUri, PromptBehavior.Auto);
string authHeader = result.CreateAuthorizationHeader();
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
//HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AppServiceBaseAddress + "/api/UserADGraphController/GetMyInformations");
//request.Headers.TryAddWithoutValidation("Authorization", authHeader);
//HttpResponseMessage response = await client.SendAsync(request);
//string responseString = await response.Content.ReadAsStringAsync();
//LogManager.log(responseString);
//Messenger.Default.Send<NotificationMessage>(new NotificationMessage(responseString));
HttpResponseMessage response = await httpClient.GetAsync(AppServiceBaseAddress + "/api/UserADGraphController/GetMyInformations");
if (response.IsSuccessStatusCode)
{
var jsonString = await response.Content.ReadAsStringAsync();
LogManager.log(jsonString);
me = JsonConvert.DeserializeObject<IUser>(jsonString);
//Messenger.Default.Send<NotificationMessage>(new NotificationMessage(jsonString));
}
In my case response has status code 200 but the jsonString contains the web page telling me about javascript disabled.
If someone has an idea it would be great !
Thanks !
If someone gets into this issue, I managed to solve it by changing my configureAuth method this way :
var azureADBearerAuthOptions = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = tenant
};
azureADBearerAuthOptions.TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = audience
};
app.UseWindowsAzureActiveDirectoryBearerAuthentication(azureADBearerAuthOptions);
This error message is very misleading. I was getting the same problem, and found that my issue was actually mismatched Client Secret/AppURI settings.
From the error message, I assumed it was something I was doing in the ConfigureAuth method. Turns out I was mixing up dev and test settings.
Maybe this will help others who end up on this confusing error message.
I've done authentication via VK, Instagram, Facebook in my site by template below.
However google requires "Redirect URL".
My redirect URL is like this:
http://localhost:4588/main/AuthenticationCallback?__provider__=google%2B&__sid__=6f3cc5957e4742758719f9b7decc2c09
Parameter "sid" is random every time. So I can't give google precise URL. I tried to input http://localhost:4588/main/AuthenticationCallback as I did for Instagram and it worked for Instagram but Google keeps showing me "400 Error: redirect_uri_mismatch"
I've also tried to pass http://localhost:4588/main/AuthenticationCallback as URL parameter in authorization url to google below. But in this case method "IAuthenticationClient.RequestAuthentication" is not called at all.
Can you advise me what should I input as "Redirect URL" for my Google app?
Template class working with OAuth2:
public class GoogleAuthenticationClient : IAuthenticationClient
{
public string appId;
public string appSecret;
private string redirectUri;
public GoogleAuthenticationClient(string appId, string appSecret)
{
this.appId = appId;
this.appSecret = appSecret;
}
string IAuthenticationClient.ProviderName
{
get { return "google+"; }
}
void IAuthenticationClient.RequestAuthentication(HttpContextBase context, Uri returnUrl)
{
var APP_ID = this.appId;
this.redirectUri = context.Server.UrlEncode(returnUrl.ToString());
var address = String.Format(
"https://accounts.google.com/o/oauth2/auth?client_id={0}&redirect_uri={1}&response_type=code&scope={2}",
APP_ID, this.redirectUri, "https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email"
);
HttpContext.Current.Response.Redirect(address, false);
}
class AccessToken
{
public string access_token = null;
public string user_id = null;
}
class UserData
{
public string uid = null;
public string first_name = null;
public string last_name = null;
public string photo_50 = null;
}
class UsersData
{
public UserData[] response = null;
}
AuthenticationResult IAuthenticationClient.VerifyAuthentication(HttpContextBase context)
{
try
{
string code = context.Request["code"];
var address = String.Format(
"https://accounts.google.com/o/oauth2/token?client_id={0}&client_secret={1}&code={2}&redirect_uri={3}",
this.appId, this.appSecret, code, this.redirectUri);
var response = GoogleAuthenticationClient.Load(address);
var accessToken = GoogleAuthenticationClient.DeserializeJson<AccessToken>(response);
address = String.Format(
"https://www.googleapis.com/plus/v1/people/{0}?access_token=1/fFBGRNJru1FQd44AzqT3Zg",
accessToken.user_id);
response = GoogleAuthenticationClient.Load(address);
var usersData = GoogleAuthenticationClient.DeserializeJson<UsersData>(response);
var userData = usersData.response.First();
return new AuthenticationResult(
true, (this as IAuthenticationClient).ProviderName, accessToken.user_id,
userData.first_name + " " + userData.last_name,
new Dictionary<string, string>());
}
catch (Exception ex)
{
return new AuthenticationResult(ex);
}
}
public static string Load(string address)
{
var request = WebRequest.Create(address) as HttpWebRequest;
using (var response = request.GetResponse() as HttpWebResponse)
{
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
return reader.ReadToEnd();
}
}
}
public static T DeserializeJson<T>(string input)
{
var serializer = new JavaScriptSerializer();
return serializer.Deserialize<T>(input);
}
}
Code in my Controller:
public void ExternalLogin(string provider)
{
OAuthWebSecurity.RegisterClient(
client: new GoogleAuthenticationClient(
"APP_ID", "APP_CODE"),
displayName: "google+", // надпись на кнопке
extraData: null);
ExternalLoginCallback(provider);
}
public void ExternalLoginCallback(string provider)
{
OAuthWebSecurity.RequestAuthentication(provider, Url.Action("AuthenticationCallback"));
}
public ActionResult AuthenticationCallback()
{
var result = OAuthWebSecurity.VerifyAuthentication();
if (result.IsSuccessful == false)
{
return null;
}
else
{
var provider = result.Provider;
var uniqueUserID = result.ProviderUserId;
return RedirectToAction("Main", "Main");
}
}
You can authorise a redirect URI as explained below, but you can't add any parameters to the redirect uri, please see this answer on how the parameters can be passed to Google google oauth2 redirect_uri with several parameters
The authorised redirect URI needs to be set when you created your client ("APP_ID", "APP_CODE") on the Google Cloud Console. Simply navigate to the API console for your project and edit the Web client to set the correct redirect URI you would like to use.
I'm experiencing a situation that I have find nowhere in other articles. I'm designing a RESTful server to be consumed by an mobile app. In this case, username and password are part of header in app call, there is no logon screen.
The following code does the job of validating user info and the controllers has security controlled.
My question is: how can I populate iPrincipal in the ApiController controllers?
I have created a filter addressed at WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Filtro de login
config.Filters.Add(new tbAuthorize());
The code for tbAuthorize is:
public class tbAuthorize : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
string username;
string password;
if (GetUserNameAndPassword(actionContext, out username, out password))
{
if (!isUserAuthorized(username, password))
return false;
else
{
//Users = username;
return true;
}
}
else
return false;
}
private bool GetUserNameAndPassword(HttpActionContext actionContext, out string username, out string password)
{
username = "";
password = "";
if (actionContext.Request.Headers.Authorization == null) return false;
// Convert 64 code to separated string[]
string[] s = ParseAuthHeader(actionContext.Request.Headers.Authorization.ToString());
if (s == null)
return false;
username = s[0];
password = s[1];
return true;
}
private string[] ParseAuthHeader(string authHeader)
{
// Check this is a Basic Auth header
if (authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic")) return null;
// Pull out the Credentials with are seperated by ':' and Base64 encoded
string base64Credentials = authHeader.Substring(6);
string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' });
if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[0])) return null;
// Okay this is the credentials
return credentials;
}
private bool isUserAuthorized(string username, string password)
{
// Valid the user at database
var userId = new UsersController().Login(username, password);
// Membership.GetUser() is null
//Users = Membership.GetUser().Email;
return userId != 0;
}
}
The issue is that I have no access to a cookie in Response and I did not find to way to populate iPrincipal.
I need to has data in this.User.Identity.Name, as this:
[tbAuthorize]
public class UsersController : ApiController
{
public void test()
{
string x = this.User.Identity.Name;
}
Thanks for any help,
Marco Castro
Authentication and Authorization are two differents things. Before authorizing a user you have to authenticate their.
With WebApi you have the concept of pipeline with Delegatinghandler. Message goes from one handler to the next until one send the response. I recommend you to create a DelegatingHandler to authentificate users. Then you can use AuthorizeAttribute to prevent unauthenticated user to access your API.
Here's an example to authenticate user with HTTP Basic
public abstract class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic Realm=\"{0}\"";
protected BasicAuthMessageHandler()
{
}
protected BasicAuthMessageHandler(HttpConfiguration httpConfiguration)
{
InnerHandler = new HttpControllerDispatcher(httpConfiguration);
}
protected virtual string GetRealm(HttpRequestMessage message)
{
return message.RequestUri.Host;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Process request
AuthenticationHeaderValue authValue = request.Headers.Authorization;
if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter) &&
string.Equals(authValue.Scheme, "basic", StringComparison.OrdinalIgnoreCase))
{
// Try to authenticate user
IPrincipal principal = ValidateHeader(authValue.Parameter);
if (principal != null)
{
request.GetRequestContext().Principal = principal;
}
}
return base.SendAsync(request, cancellationToken) // Send message to the InnerHandler
.ContinueWith(task =>
{
// Process response
var response = task.Result;
if (response.StatusCode == HttpStatusCode.Unauthorized &&
!response.Headers.Contains(BasicAuthResponseHeader))
{
response.Headers.Add(BasicAuthResponseHeader,
string.Format(BasicAuthResponseHeaderValue, GetRealm(request)));
}
return response;
}, cancellationToken);
}
private IPrincipal ValidateHeader(string authHeader)
{
// Decode the authentication header & split it
var fromBase64String = Convert.FromBase64String(authHeader);
var lp = Encoding.Default.GetString(fromBase64String);
if (string.IsNullOrWhiteSpace(lp))
return null;
string login;
string password;
int pos = lp.IndexOf(':');
if (pos < 0)
{
login = lp;
password = string.Empty;
}
else
{
login = lp.Substring(0, pos).Trim();
password = lp.Substring(pos + 1).Trim();
}
return ValidateUser(login, password);
}
protected abstract IPrincipal ValidateUser(string userName, string password);
}
Write you own user validation logic. For example:
public class SampleBasicAuthMessageHandler : BasicAuthMessageHandler
{
protected override IPrincipal ValidateUser(string userName, string password)
{
if (string.Equals(userName, "Meziantou", StringComparison.OrdinalIgnoreCase) && password == "123456")
return new GenericPrincipal(new GenericIdentity(userName, "Basic"), new string[0]);
return null;
}
}
Finally you have to register the Handler
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.MessageHandlers.Add(new SampleBasicAuthMessageHandler());
You'll find a complete example on Github: https://github.com/meziantou/Samples/tree/master/Web%20Api%20-%20Basic%20Authentication
In the last DotNetOpenAuth package, GoogleClient extends OpenIdClient, Someone knows where can i find implementation of google Oauth2 which extends DotNetOpenAuth OAuth2Client?
From OAuth2 and DotNetOpenAuth - implementing Google custom client
public class GoogleOAuth2Client : OAuth2Client
{
#region Constants and Fields
/// <summary>
/// The authorization endpoint.
/// </summary>
private const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth";
/// <summary>
/// The token endpoint.
/// </summary>
private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
/// <summary>
/// The _app id.
/// </summary>
private readonly string _clientId;
/// <summary>
/// The _app secret.
/// </summary>
private readonly string _clientSecret;
#endregion
public const string ProviderAppendix = "__provider__=google";
public GoogleOAuth2Client(string clientId, string clientSecret)
: base("google")
{
if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException("clientId");
if (string.IsNullOrWhiteSpace(clientSecret)) throw new ArgumentNullException("clientSecret");
this._clientId = clientId;
this._clientSecret = clientSecret;
}
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
StringBuilder serviceUrl = new StringBuilder();
serviceUrl.AppendFormat("{0}?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile", AuthorizationEndpoint);
serviceUrl.Append("&state=google");
serviceUrl.AppendFormat("&redirect_uri={0}", returnUrl.ToString().ToLower());
serviceUrl.Append("&response_type=code");
serviceUrl.AppendFormat("&client_id={0}", _clientId);
return new Uri(serviceUrl.ToString());
}
protected override IDictionary<string, string> GetUserData(string accessToken)
{
var client = new RestClient("https://www.googleapis.com");
var request = new RestRequest(String.Format("/oauth2/v1/userinfo?access_token={0}", accessToken), Method.GET);
IDictionary<String, String> extraData = new Dictionary<String, String>();
var response = client.Execute(request);
if (null != response.ErrorException)
{
return null;
}
else
{
try
{
var json = JObject.Parse(response.Content);
string firstName = (string)json["given_name"];
string lastName = (string)json["family_name"];
string emailAddress = (string)json["email"];
string id = (string)json["id"];
extraData = new Dictionary<String, String>
{
{"accesstoken", accessToken},
{"name", String.Format("{0} {1}", firstName, lastName)},
{"firstname", firstName},
{"lastname", lastName},
{"email", emailAddress},
{"id", id}
};
}
catch (Exception ex)
{
Ccl.Log.Logging.Error("Error requesting OAuth user data from Google", ex);
return null;
}
return extraData;
}
}
protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
{
StringBuilder postData = new StringBuilder();
postData.AppendFormat("client_id={0}", this._clientId);
postData.AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(returnUrl.ToString().ToLower()));
postData.AppendFormat("&client_secret={0}", this._clientSecret);
postData.AppendFormat("&grant_type={0}", "authorization_code");
postData.AppendFormat("&code={0}", authorizationCode);
string response = "";
string accessToken = "";
var webRequest = (HttpWebRequest)WebRequest.Create(TokenEndpoint);
webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
try
{
using (Stream s = webRequest.GetRequestStream())
{
using (StreamWriter sw = new StreamWriter(s))
sw.Write(postData.ToString());
}
using (WebResponse webResponse = webRequest.GetResponse())
{
using (var reader = new StreamReader(webResponse.GetResponseStream()))
{
response = reader.ReadToEnd();
}
}
var json = JObject.Parse(response);
accessToken = (string)json["access_token"];
}
catch (Exception ex)
{
Ccl.Log.Logging.Error("Error requesting OAuth access token from Google", ex);
return null;
}
return accessToken;
}
public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl)
{
string code = context.Request.QueryString["code"];
if (string.IsNullOrEmpty(code))
{
return AuthenticationResult.Failed;
}
string accessToken = this.QueryAccessToken(returnPageUrl, code);
if (accessToken == null)
{
return AuthenticationResult.Failed;
}
IDictionary<string, string> userData = this.GetUserData(accessToken);
if (userData == null)
{
return AuthenticationResult.Failed;
}
string id = userData["id"];
string name;
// Some oAuth providers do not return value for the 'username' attribute.
// In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id'
if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name))
{
name = id;
}
// add the access token to the user data dictionary just in case page developers want to use it
userData["accesstoken"] = accessToken;
return new AuthenticationResult(
isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData);
}
}