Firebase Delete User who signed it with apple correclty - ios

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;
}
}

Related

How to specify more than one IssuerSigningKey in UseJwtBearerAuthentication?

I have a REST api using OAuth bearer token authentication. Token is signed by an asymmetric key and REST api validates the token using public key. I got the code work like below. However, there is a case I need to handle when key needs to update. I am thinking to have a secondary public key passed in and let the framework validate token first using primary key and then secondary key. In this way, when I need to update key, I can easily add secondary key, swap and retire. The issue is looking at code below it only takes one signing key. Is there a way to specify multiple?
public void ConfigureAuth(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
// codes to get signningKey ignored here
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKey = new RsaSecurityKey(signingKey)
},
});
}
Thanks,
Ok, I think I figured it out. There are two ways. One simply straight forward way is to use IssuerSigningKeys property (how could I not discovery it at first place). The code looks like this:
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKeys = new List<RsaSecurityKey>
{
Utils.GetSigningKey(isPrimary: true),
Utils.GetSigningKey(isPrimary: false)
},
},
});
The second approach is to customized IOAuthBearerAuthenticationProvider. The code looks like this: First,
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AllowedAudiences = new string[] { "*" },
IssuerSecurityTokenProviders = new List<IIssuerSecurityTokenProvider>()
{
// Dummy object which won't be used anywhere. It is used to work around parameter validation error about no token provider specified.
new SymmetricKeyIssuerSecurityTokenProvider("dummy", "dummy")
},
// This is where validation work happens.
Provider = new BearerAuthenticationProvider(app)
});
Then, the BearerAuthenticationProvider class:
/// <summary>
/// Bearer authentication provider.
/// </summary>
public class BearerAuthenticationProvider : IOAuthBearerAuthenticationProvider
{
/// <summary>
/// App config.
/// </summary>
private readonly IAppBuilder appConfig;
/// <summary>
/// Handles applying the authentication challenge to the response message.
/// </summary>
public Func<OAuthChallengeContext, Task> OnApplyChallenge { get; set; }
/// <summary>
/// Handles processing OAuth bearer token.
/// </summary>
public Func<OAuthRequestTokenContext, Task> OnRequestToken { get; set; }
/// <summary>
/// Handles validating the identity produced from an OAuth bearer token.
/// </summary>
public Func<OAuthValidateIdentityContext, Task> OnValidateIdentity { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="T:Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationProvider" /> class
/// </summary>
public BearerAuthenticationProvider(IAppBuilder appConfig)
{
this.appConfig = appConfig;
this.OnRequestToken = (OAuthRequestTokenContext context) =>
{
var idContext = new OAuthValidateIdentityContext(context.OwinContext, null, null);
this.ValidateIdentity(idContext);
return Task.FromResult<int>(0);
};
this.OnValidateIdentity = (OAuthValidateIdentityContext context) => Task.FromResult<object>(null);
this.OnApplyChallenge = (OAuthChallengeContext context) => Task.FromResult<object>(null);
}
/// <summary>
/// Handles applying the authentication challenge to the response message.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public Task ApplyChallenge(OAuthChallengeContext context)
{
return this.OnApplyChallenge(context);
}
/// <summary>
/// Handles processing OAuth bearer token.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task RequestToken(OAuthRequestTokenContext context)
{
return this.OnRequestToken(context);
}
/// <summary>
/// Handles validating the identity produced from an OAuth bearer token.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task ValidateIdentity(OAuthValidateIdentityContext context)
{
const string AuthHeaderName = "Authorization";
if (context.Request.Headers.ContainsKey(AuthHeaderName))
{
var jwt = context.Request.Headers[AuthHeaderName].Replace("Bearer ", string.Empty);
var token = new JwtSecurityToken(jwt);
var claimIdentity = new ClaimsIdentity(token.Claims, "ExternalBearer");
var param = new TokenValidationParameters()
{
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKeys = new List<RsaSecurityKey>
{
Utils.GetSigningKey(isPrimary: true),
Utils.GetSigningKey(isPrimary: false)
},
};
SecurityToken securityToken = null;
var handler = new JwtSecurityTokenHandler();
var identity = handler.ValidateToken(token.RawData, param, out securityToken);
var claimPrincipal = new ClaimsPrincipal(claimIdentity);
context.Response.Context.Authentication.User = claimPrincipal;
context.Validated(claimIdentity);
}
else
{
throw new Exception("Invalid authorization header.");
}
return this.OnValidateIdentity(context);
}
}
First approach initializes two signing keys at app startup and only way to make change is when process restarts. the second approach retrieves keys at run time so key rollover doesn't require a service restart.
If you would like to have multiple security keys, you can use the benefit of IssuerSigningKeys property, where you can add all the keys you would like to use for authentication.

ASP.NET Identity, persistent cookie - is something like this build in?

We are using CookieAuthenticationProvider and would like to implement the 'Remember me' functionality in our application that would work like this:
No matter if the 'Remember me' checkbox is checked or not, the token expiration time should always be set to 30 minutes (with SlidingExpiration turned on)
If user doesn't check 'Remember me' all we do is check if token expired - if it did, then user is redirected to login screen (this is build in into OWIN and works fine)
However if user checks 'Remember me' his credentials should be saved in the additional cookie (with default lifetime of 30 days). If his token expires (the timeout should still be set to 30 minutes), OWIN should use that additional cookie to renew the token automatically in the background. So in other words - if user check 'Remember me' he should be logged in for 30 days or until he logs out.
Question is - how can something like this be done with OWIN? As far as I can see, the default implementation still uses ExpireTimeSpan parameter - the only difference is, that the cookie is marked as persistent, so if user restarts browser he is logged in - but token expiration is still limited by ExpireTimeSpan.
I guess I have to somehow manually save user credentials during the SignIn and override the OnApplyRedirect event (that seems to be the only event fired if an unauthorized user tries to access a view that requires authorization), and instead of redirecting, somehow regenerate user's token... but does anybody know how exactly to do that?
Finally, I ended up writing custom middleware and plugging it in:
RememberMeTokenMiddleware.cs:
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Security;
using WebApplicationtoRemove.Owin.HelperClasses;
using Microsoft.AspNet.Identity.Owin;
namespace WebApplicationtoRemove.Owin.Middleware
{
public class RememberMeTokenMiddleware : OwinMiddleware
{
#region Private Members
private static double RememberMeTokenPeriodOfvalidityInMinutes = 43200;
private IOwinContext Context { get; set; }
#endregion
#region Public Static Members
#endregion
#region Constructor
public RememberMeTokenMiddleware(OwinMiddleware next)
: base(next)
{
}
public RememberMeTokenMiddleware(OwinMiddleware next, double RememberMeTokenPeriodOfvalidityInMinutes)
: base(next)
{
RememberMeTokenMiddleware.RememberMeTokenPeriodOfvalidityInMinutes = RememberMeTokenPeriodOfvalidityInMinutes;
}
#endregion
#region Public Methods
public override async Task Invoke(IOwinContext context)
{
try
{
Context = context;
bool shouldDeleteRememberMeToken = CheckIfRememberMeTokenShouldBeDeleted(context);
if (shouldDeleteRememberMeToken)
{
context.Response.Cookies.Delete("RemoveRememberMeToken");
context.Response.Cookies.Delete("RememberMeToken");
}
else
{
if (context.Authentication.User == null || !context.Authentication.User.Identity.IsAuthenticated)
{
//User is either not set or is not authenticated - try to log him in, using the RememberMeCookie
Login(context);
}
}
}
catch (Exception ex)
{
//Something went wrong - we assume that cookie and/or token was damaged and should be deleted
context.Response.Cookies.Delete("RememberMeToken");
}
await this.Next.Invoke(context);
}
#endregion
#region Static Methods
/// <summary>
/// Check conditions and creates RememberMeToken cookie if necessary. This should be called inside SidnedIn event of CookieProvider
/// </summary>
public static void CheckAndCreateRememberMeToken(CookieResponseSignedInContext ctx)
{
try
{
bool signedInFromRememberMeToken = CheckIfUserWasSignedInFromRememberMeToken(ctx.OwinContext);
if (!signedInFromRememberMeToken && ctx.Properties.IsPersistent)
{
//Login occured using 'normal' path and IsPersistant was set - generate RememberMeToken cookie
var claimsToAdd = GenerateSerializableClaimListFromIdentity(ctx.Identity);
SerializableClaim cookieExpirationDate = GenerateRememberMeTokenExpirationDateClaim();
claimsToAdd.Add(cookieExpirationDate);
var allClaimsInFinalCompressedAndProtectedBase64Token = GenerateProtectedAndBase64EncodedClaimsToken(claimsToAdd);
ctx.Response.Cookies.Append("RememberMeToken", allClaimsInFinalCompressedAndProtectedBase64Token, new CookieOptions()
{
Expires = DateTime.Now.AddMinutes(RememberMeTokenPeriodOfvalidityInMinutes)
});
//Remove the SignedInFromRememberMeToken cookie, to let the middleware know, that user was signed in using normal path
ctx.OwinContext.Set("SignedInFromRememberMeToken", false);
}
}
catch (Exception ex)
{
//Log errors using your favorite logger here
}
}
/// <summary>
/// User logged out - sets information (using cookie) for RememberMeTokenMiddleware that RememberMeToken should be removed
/// </summary>
public static void Logout(IOwinContext ctx)
{
ctx.Response.Cookies.Append("RemoveRememberMeToken", "");
}
#endregion
#region Private Methods
/// <summary>
/// Returns information if user was signed in from RememberMeToken cookie - this information should be used to determine if RememberMeToken lifetime should be regenerated or not (it should be, if user signed in using normal path)
/// </summary>
private static bool CheckIfUserWasSignedInFromRememberMeToken(IOwinContext ctx)
{
bool signedInFromRememberMeToken = ctx.Get<bool>("SignedInFromRememberMeToken");
return signedInFromRememberMeToken;
}
/// <summary>
/// Generates serializable collection of user claims, that will be saved inside the cookie token. Custom class is used because Claim class causes 'Circular Reference Exception.'
/// </summary>
private static List<SerializableClaim> GenerateSerializableClaimListFromIdentity(ClaimsIdentity identity)
{
var dataToReturn = identity.Claims.Select(x =>
new SerializableClaim()
{
Type = x.Type,
ValueType = x.ValueType,
Value = x.Value
}).ToList();
return dataToReturn;
}
/// <summary>
/// Generates a special claim containing an expiration date of RememberMeToken cookie. This is necessary because we CANNOT rely on browsers here - since each one threat cookies differently
/// </summary>
private static SerializableClaim GenerateRememberMeTokenExpirationDateClaim()
{
SerializableClaim cookieExpirationDate = new SerializableClaim()
{
Type = "RememberMeTokenExpirationDate",
Value = DateTime.Now.AddMinutes(RememberMeTokenPeriodOfvalidityInMinutes).ToBinary().ToString()
};
return cookieExpirationDate;
}
/// <summary>
/// Generates token containing user claims. The token is compressed, encrypted using machine key and returned as base64 string - this string will be saved inside RememberMeToken cookie
/// </summary>
private static string GenerateProtectedAndBase64EncodedClaimsToken(List<SerializableClaim> claimsToAdd)
{
var allClaimsAsString = JsonConvert.SerializeObject(claimsToAdd);
var allClaimsAsBytes = Encoding.UTF8.GetBytes(allClaimsAsString);
var allClaimsAsCompressedBytes = CompressionHelper.CompressDeflate(allClaimsAsBytes);
var allClaimsAsCompressedBytesProtected = MachineKey.Protect(allClaimsAsCompressedBytes, "RememberMeToken");
var allClaimsInFinalCompressedAndProtectedBase64Token = Convert.ToBase64String(allClaimsAsCompressedBytesProtected);
return allClaimsInFinalCompressedAndProtectedBase64Token;
}
/// <summary>
/// Primary login method
/// </summary>
private void Login(IOwinContext context)
{
var base64ProtectedCompressedRememberMeTokenBytes = context.Request.Cookies["RememberMeToken"];
if (!string.IsNullOrEmpty(base64ProtectedCompressedRememberMeTokenBytes))
{
var RememberMeToken = GetRememberMeTokenFromData(base64ProtectedCompressedRememberMeTokenBytes);
var claims = JsonConvert.DeserializeObject<IEnumerable<SerializableClaim>>(RememberMeToken);
bool isRememberMeTokenStillValid = IsRememberMeTokenStillValid(claims);
if (isRememberMeTokenStillValid)
{
//Token is still valid - sign in
SignInUser(context, claims);
//We set information that user was signed in using the RememberMeToken cookie
context.Set("SignedInFromRememberMeToken", true);
}
else
{
//Token is invalid or expired - we remove unnecessary cookie
context.Response.Cookies.Delete("RememberMeToken");
}
}
}
/// <summary>
/// We log user, using passed claims
/// </summary>
private void SignInUser(IOwinContext context, IEnumerable<SerializableClaim> claims)
{
List<Claim> claimList = new List<Claim>();
foreach (var item in claims)
{
string type = item.Type;
string value = item.Value;
claimList.Add(new Claim(type, value));
}
ClaimsIdentity ci = new ClaimsIdentity(claimList, DefaultAuthenticationTypes.ApplicationCookie);
context.Authentication.SignIn(ci);
context.Authentication.User = context.Authentication.AuthenticationResponseGrant.Principal;
}
/// <summary>
/// Get information if RememberMeToken cookie is still valid (checks not only the date, but also some additional information)
/// </summary>
private bool IsRememberMeTokenStillValid(IEnumerable<SerializableClaim> claims)
{
var userIdClaim = claims.Where(x => x.Type == ClaimTypes.NameIdentifier).SingleOrDefault();
if (userIdClaim == null)
{
throw new Exception("RememberMeTokenAuthMiddleware. Claim of type NameIdentifier was not found.");
}
var userSecurityStampClaim = claims.Where(x => x.Type == "AspNet.Identity.SecurityStamp").SingleOrDefault();
if (userSecurityStampClaim == null)
{
throw new Exception("RememberMeTokenAuthMiddleware. Claim of type SecurityStamp was not found.");
}
string userId = userIdClaim.Value;
var userManager = Context.GetUserManager<ApplicationUserManager>();
if (userManager == null)
{
throw new Exception("RememberMeTokenAuthMiddleware. Unable to get UserManager");
}
var currentUserData = userManager.FindById(userId);
if (currentUserData == null)
{
return false;
}
if (currentUserData.LockoutEndDateUtc >= DateTime.Now)
{
return false;
}
if (currentUserData.SecurityStamp != userSecurityStampClaim.Value)
{
//User Securitystamp was changed
return false;
}
return GetRememberMeTokenExpirationMinutesLeft(claims) > 0;
}
/// <summary>
/// Returns how many minutes the RememberMeToken will be valid - if it expired, returns zero or negative value
/// </summary>
private double GetRememberMeTokenExpirationMinutesLeft(IEnumerable<SerializableClaim> claims)
{
double dataToReturn = -1;
var RememberMeTokenExpirationDate = GetRememberMeTokenExpirationDate(claims);
dataToReturn = (RememberMeTokenExpirationDate - DateTime.Now).TotalMinutes;
return dataToReturn;
}
/// <summary>
/// Returns a DateTime object containing the expiration date of the RememberMeToken
/// </summary>
private DateTime GetRememberMeTokenExpirationDate(IEnumerable<SerializableClaim> claims)
{
DateTime RememberMeTokenExpirationDate = DateTime.Now.AddDays(-1);
var RememberMeTokenExpirationClaim = GetRememberMeTokenExpirationDateClaim(claims);
if (RememberMeTokenExpirationClaim == null)
{
throw new Exception("RememberMeTokenAuthMiddleware. RememberMeTokenExpirationClaim was not found.");
}
long binaryTime = Convert.ToInt64(RememberMeTokenExpirationClaim.Value);
RememberMeTokenExpirationDate = DateTime.FromBinary(binaryTime);
return RememberMeTokenExpirationDate;
}
/// <summary>
/// Returns the claim determining the expiration date of the token
/// </summary>
private SerializableClaim GetRememberMeTokenExpirationDateClaim(IEnumerable<SerializableClaim> claims)
{
var RememberMeTokenExpirationClaim = claims.Where(x => x.Type == "RememberMeTokenExpirationDate").SingleOrDefault();
return RememberMeTokenExpirationClaim;
}
/// <summary>
/// Attempts to decipher the RememberMeToken to the JSON format containing claims
/// </summary>
private string GetRememberMeTokenFromData(string base64ProtectedCompressedRememberMeTokenBytes)
{
var protectedCompressedRememberMeTokenBytes = Convert.FromBase64String(base64ProtectedCompressedRememberMeTokenBytes);
var compressedRememberMeTokenBytes = MachineKey.Unprotect(protectedCompressedRememberMeTokenBytes, "RememberMeToken");
var RememberMeTokenBytes = CompressionHelper.DecompressDeflate(compressedRememberMeTokenBytes);
var RememberMeToken = Encoding.UTF8.GetString(RememberMeTokenBytes);
return RememberMeToken;
}
/// <summary>
/// Returns information if token cookie should be delated (for example, when user click 'Logout')
/// </summary>
private bool CheckIfRememberMeTokenShouldBeDeleted(IOwinContext context)
{
bool shouldDeleteRememberMeToken = (context.Request.Cookies.Where(x => x.Key == "RemoveRememberMeToken").Count() > 0);
return shouldDeleteRememberMeToken;
}
#endregion
}
}
And some helper classes:
CompressionHelper.cs:
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Web;
namespace WebApplicationtoRemove.Owin.HelperClasses
{
/// <summary>
/// Data compression helper
/// </summary>
public static class CompressionHelper
{
public static byte[] CompressDeflate(byte[] data)
{
MemoryStream output = new MemoryStream();
using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Optimal))
{
dstream.Write(data, 0, data.Length);
}
return output.ToArray();
}
public static byte[] DecompressDeflate(byte[] data)
{
MemoryStream input = new MemoryStream(data);
MemoryStream output = new MemoryStream();
using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress))
{
dstream.CopyTo(output);
}
return output.ToArray();
}
}
}
SerializableClaim.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace WebApplicationtoRemove.Owin.HelperClasses
{
public class SerializableClaim
{
public string Type { get; set; }
public string ValueType { get; set; }
public string Value { get; set; }
}
}
To test the above - create new MVC 4.6.x project (authentication mode: Individual User Accounts), add the above classes to it and then modify the Startup.Auth.cs:
using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Google;
using Owin;
using WebApplicationtoRemove.Models;
using WebApplicationtoRemove.Owin.Middleware;
namespace WebApplicationtoRemove
{
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.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
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnResponseSignedIn = ctx =>
{
RememberMeTokenMiddleware.CheckAndCreateRememberMeToken(ctx);
},
OnResponseSignOut = ctx =>
{
RememberMeTokenMiddleware.Logout(ctx.OwinContext);
}
}
});
app.Use<RememberMeTokenMiddleware>();
}
}
}
What interests you there are these:
OnResponseSignedIn = ctx =>
{
RememberMeTokenMiddleware.CheckAndCreateRememberMeToken(ctx);
},
OnResponseSignOut = ctx =>
{
RememberMeTokenMiddleware.Logout(ctx.OwinContext);
}
and this line:
app.Use<RememberMeTokenMiddleware>();
This should enable the middleware. How this works: if the user checks 'Remember me' checkbox, a RememberMeToken cookie will be created (containing all the claims user had during login) alongside the 'AspNet.ApplicationCookie'.
When the session times out, the middleware will check if the RememberMeToken exists, and is still valid - if so: it will log in the user seamlessly in background.
Hope this helps anyone.

Troubles authenticating native client to Azure AD securised MVC web app

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.

OAuth access and refresh token control / management on user password change

We are in the process of developing a in house mobile application and web api.
We are using asp.net web api 2 with asp.net Identy 2 OAuth.
I have got the api up and running and giving me a bearer token. However I want to slightly modify the process flow to something like along the lines of this:
App user logs in to api with username and password.
App receives Refresh-token which is valid for 30 days.
App then requests an access token providing the api with the refresh token. ( Here I want to be able to invalidate a request if the user has changed their password or their account has been locked).
App Gets An Access token which is valid for 30 minutes or it gets a 401 if the password check failed.
The App can access the api with the given access token for the next 29 minutes. After that the app will have to get a new access token with the refresh token.
Reason I want to do this is in order to stop a users devices gaining access to the api after they have changed their password. If their phone gets stolen they need to be able to login to the website and change their password so the new owner of the phone cannot gain access to our companies services.
Is my proposed solution do able, and if so is it a sensible solution? I haven't forgotten any crucial elements?
I am willing to do a db access on ever token refresh, but not on every API call.
To summarize my questions are:
Is my planned method sensible?
How would I securely check if the password has changed or if account is locked in the refresh token process.
Please find my current OAuth Setups and classes below: (I have tried to add the refresh token functionality, but have not attempted to add any password verification yet)
Startup.Auth.cs
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(IdentityDbContext.Create);
app.CreatePerOwinContext<FskUserManager>(FskUserManager.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.UseCookieAuthentication(new CookieAuthenticationOptions());
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),
RefreshTokenProvider = new ApplicationRefreshTokenProvider(),
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
ApplicationRefreshTokenProvider.cs
public class ApplicationRefreshTokenProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
// Expiration time in minutes
int refreshTokenExpiration = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["ApiRefreshTokenExpiry"]);
context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddMinutes(refreshTokenExpiration));
context.SetToken(context.SerializeTicket());
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
}
ApplicationOAuthProvider.cs
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
public ApplicationOAuthProvider(string publicClientId)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
_publicClientId = publicClientId;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<FskUserManager>();
FskUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Resource owner password credentials does not provide a client ID.
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
Uri expectedRootUri = new Uri(context.Request.Uri, "/");
if (expectedRootUri.AbsoluteUri == context.RedirectUri)
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
If I understood your task right, here is an idea.
On the create access token event, you can check if the password has been changed from the website and if so, revoke the refresh token. (you can create some flag that the password has been changed or something)
It should not be often when you are creating an access token so there should be no problems with the db access.
Now the question is how to revoke a refresh token. Unless there is a build in way you will have to implement a custom one. An idea here is to check the refresh token creation date and the date of the change password operation. If the change password operation is done after the creation of the refresh token, you do not authenticate the user.
Let me know what you think of this.

Where do I store the Web API access token?

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

Resources