I am trying to make a custom filter for my web api controllers part of a ASP.NET MVC 5 website/application to check the requests headers for a specific token which i have stored in the database. Most examples I found were containing user credentials and then the user was authenticated using identity. Not exactly what I am looking for.
This is the tutorial I found and am currently following.
The web API should only handle "external" HTTP calls, the website side will have its own controllers presently (but may be subject to change).
This filter should interface with identity 2 system already present if possible.
What I do is that I send user credentials then, assign a token to the user and then I want to use that token to authenticate the request. Is there a way I can just filter the request based on the token or do I need to use Owin identity and their token management. I am using a mobile client (currently iOS, will include android too) to make the calls. Any example or tutorial I could refer to ?
The token is currently a random combination of alphanumeric characters and symbols.
Thank you.
P.S. I can post code snippets and stuff where needed.
Edit: The HTTPRequests will be filtered based on whether they contain a token present within our database/system. Requests that do contain a token or are not present within our system will receive unauthorised error (401?)
Suppose you thought that sending user name and password to every request is not good.Refer my below implementation with out user name and password, since we are don't send user name and password with every request.
public class AuthenticationFilter : AuthorizationFilterAttribute
{
/// <summary>
/// read requested header and validated
/// </summary>
/// <param name="actionContext"></param>
public override void OnAuthorization(HttpActionContext actionContext)
{
var identity = FetchFromHeader(actionContext);
if(identity != null)
{
var securityService = actionContext.ControllerContext.Configuration.DependencyResolver.GetService(typeof(ILoginService)) as ILoginService;
if (securityService.TokenAuthentication(identity))
{
CurrentThread.SetPrincipal(new GenericPrincipal(new GenericIdentity(identity), null), null, null);
}
else
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
return;
}
}
else
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
return;
}
base.OnAuthorization(actionContext);
}
/// <summary>
/// retrive header detail from the request
/// </summary>
/// <param name="actionContext"></param>
/// <returns></returns>
private string FetchFromHeader(HttpActionContext actionContext)
{
string requestToken = null;
var authRequest = actionContext.Request.Headers.Authorization;
if (authRequest != null && !string.IsNullOrEmpty(authRequest.Scheme) && authRequest.Scheme == "Basic")
requestToken = authRequest.Parameter;
return requestToken;
}
}
You can make this filter unit testable by injecting the service dependencies via property(property injection). For custom attributes, we don't want to pass the dependencies via constructor. We want the attribute easy to
use. To rewrite what #Raj already started, it can look like this:
public class AuthenticationFilter : AuthorizationFilterAttribute
{
[Dependency]
public ILoginService LoginService { get; set; }
/// <summary>
/// read requested header and validated
/// </summary>
/// <param name="actionContext"></param>
public override void OnAuthorization(HttpActionContext actionContext)
{
var identity = FetchFromHeader(actionContext);
if (identity != null)
{
if (LoginService.TokenAuthentication(identity))
{
CurrentThread.SetPrincipal(new GenericPrincipal(new GenericIdentity(identity), null), null, null);
//IPrincipal principal = new GenericPrincipal(new GenericIdentity(identity), new string[] { "myRole" });
//Thread.CurrentPrincipal = principal;
//HttpContext.Current.User = principal;
}
else
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
return;
}
}
else
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
return;
}
base.OnAuthorization(actionContext);
}
/// <summary>
/// retrive header detail from the request
/// </summary>
/// <param name="actionContext"></param>
/// <returns></returns>
private string FetchFromHeader(HttpActionContext actionContext)
{
string requestToken = null;
var authRequest = actionContext.Request.Headers.Authorization;
if (authRequest != null && !string.IsNullOrEmpty(authRequest.Scheme) && authRequest.Scheme == "Basic")
requestToken = authRequest.Parameter;
return requestToken;
}
}
Related
My team and I created a custom OAuth to be used for external SSO. It works on localhost but as soon as we take it up to our staging environment we get an "The oauth state was missing or invalid." error.
We used "https://auth0.com/" for testing.
To try and troubleshoot this we overrode the following built in methods and via breakpoints can see that Query state comes back null.
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri);
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync();
I need some help figuring out why this is a problem on staging and not on local as we are a bit stumped. A theory we have is that the decoder used inside these methods change on var properties = Options.StateDataFormat.Unprotect(state); and thus because they aren't the same they can't decode each others states. I will put our implementation below, if its required I can paste the built in methods as well but I can't fathom the problem lying with the built in functions.
Startup:
foreach (var customAuthItem in customAuthList)
{
services.AddAuthentication().AddCustom(customAuthItem.CampaignId, options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.AuthorizationEndpoint = customAuthItem.AuthEndpoint;
options.TokenEndpoint = customAuthItem.TokenEndpoint;
options.UserInformationEndpoint = customAuthItem.UserInfoEndpoint;
options.ClientId = customAuthItem.ClientId;
options.ClientSecret = customAuthItem.ClientSecret;
});
}
Options:
public class CustomAuthenticationOptions : OAuthOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomAuthenticationOptions"/> class.
/// </summary>
public CustomAuthenticationOptions()
{
ClaimsIssuer = CustomAuthenticationDefaults.Issuer;
CallbackPath = CustomAuthenticationDefaults.CallbackPath;
AuthorizationEndpoint = CustomAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = CustomAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = CustomAuthenticationDefaults.UserInformationEndpoint;
Scope.Add("openid");
Scope.Add("profile");
Scope.Add("email");
ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
}
/// Gets the list of fields to retrieve from the user information endpoint.
/// </summary>
public ISet<string> Fields { get; } = new HashSet<string>
{
"email",
"name",
"sub"
};
Defaults:
public static class CustomAuthenticationDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationScheme.Name"/>.
/// </summary>
public const string AuthenticationScheme = "Custom";
/// <summary>
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
/// </summary>
public static readonly string DisplayName = "Custom";
/// <summary>
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
/// </summary>
public static readonly string Issuer = "Custom";
/// <summary>
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
/// </summary>
public static readonly string CallbackPath = "/signin-custom";
/// <summary>
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
public static readonly string AuthorizationEndpoint = "https://dev-egd511ku.us.auth0.com/authorize";
/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
public static readonly string TokenEndpoint = "https://dev-egd511ku.us.auth0.com/oauth/token";
/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
public static readonly string UserInformationEndpoint = "https://dev-egd511ku.us.auth0.com/userinfo";
}
Handler:
protected override async Task<AuthenticationTicket> CreateTicketAsync(
[NotNull] ClaimsIdentity identity,
[NotNull] AuthenticationProperties properties,
[NotNull] OAuthTokenResponse tokens)
{
Serilog.Log.Debug("CustomAuthenticationHandler.CreateTicketAsync: STARTED!");
string endpoint = Options.UserInformationEndpoint;
if (Options.Fields.Count > 0)
{
endpoint = QueryHelpers.AddQueryString(endpoint, "fields", string.Join(',', Options.Fields));
}
using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
Serilog.Log.Debug("CustomAuthenticationHandler.CreateTicketAsync: ABOUT TO SEND REQUEST!");
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Serilog.Log.Debug($"CustomAuthenticationHandler.CreateTicketAsync: FAILED REQUEST: {response.ReasonPhrase}");
await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
throw new HttpRequestException("An error occurred while retrieving the user profile from Custom.");
}
var payloadString = await response.Content.ReadAsStringAsync();
Serilog.Log.Debug($"CustomAuthenticationHandler.CreateTicketAsync: PAYLOAD: {payloadString}");
using var payload = JsonDocument.Parse(payloadString);// Context.RequestAborted));
var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
context.RunClaimActions();
await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
}
EDIT: The error is received after successful login and after being redirected back to our site. I can see through sentry breadcrumbs that the states are correct so it seems to be a decryption issue.
It turns out the problem is because I AddAuthentication() twice, it ignores the follow-up registration of the auth methods, resulting in only one OAuth working. This is a bit of a problem because we want to support multiple SSO options for our clients, but might need to figure out a different approach. I am just glad I finally know where the problem is.
I am using RestSharp to make requests to an API which uses bearer token authentication. Most requests run as expected, but there is a specific endpoint which always redirects the request to a dynamic location. When this redirect occurs, the Authorization header is lost (by design), thus resulting in a Bad Request.
I've done some looking into the issue and found one similar issue here, but the custom AuthenticationModule I made is never having the Authenticate function called.
Am I missing something obvious in the setup that is preventing the Authentication module from being used, or is something else going on?
Thanks!
My Authenticator class:
public class AdpAuthenticator : IAuthenticator
{
/// <summary>
/// The current access token for making requests to the API.
/// </summary>
private static string AccessToken { get; set; }
/// <summary>
/// When the current access token expires.
/// </summary>
private static DateTime TokenExpiresOn { get; set; }
private static CredentialCache CredentialCache { get; set; }
/// <summary>
/// Singleton instance for making requests for access tokens.
/// </summary>
private static IRestClient AuthenticationClient { get; set; }
/// <summary>
/// Singleton instance of the request for obtaining access tokens.
/// </summary>
private static IRestRequest AuthenticationRequest { get; set; }
/// <summary>
/// Construct a new AdpAuthenticator.
/// </summary>
/// <param name="adpClientId"></param>
/// <param name="adpClientSecret"></param>
/// <param name="adpCertPath"></param>
public AdpAuthenticator(string adpClientId, string adpClientSecret, string adpCertPath)
{
if (string.IsNullOrWhiteSpace(adpClientId)) throw new ArgumentNullException("Passed adpClientId was empty or null.");
if (string.IsNullOrWhiteSpace(adpClientSecret)) throw new ArgumentNullException("Passed adpClientSecret was empty or null.");
if (CredentialCache == null)
{
CredentialCache = new CredentialCache
{
{new Uri("https://api.adp.com"), "Basic", new NetworkCredential(adpClientId, adpClientSecret) }
};
}
if (AuthenticationClient == null)
{
X509Certificate2Collection certificateCollection;
X509Certificate2 certificate = new X509Certificate2(adpCertPath);
certificateCollection = new X509Certificate2Collection
{
certificate
};
AuthenticationClient = new RestClient("https://api.adp.com")
{
ClientCertificates = certificateCollection,
Authenticator = new HttpBasicAuthenticator(adpClientId, adpClientSecret)
};
AuthenticationClient.UseSerializer(new JsonNetSerializer());
}
if (AuthenticationRequest == null)
{
AuthenticationRequest = new RestRequest("auth/oauth/v2/token", Method.POST)
{
Credentials = CredentialCache
};
AuthenticationRequest.AddOrUpdateParameter("grant_type", "client_credentials", ParameterType.QueryString);
}
RegisterAuthenticationModule(new Uri("https://api.adp.com/"));
}
/// <summary>
/// Authenticate a request.
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
public void Authenticate(IRestClient client, IRestRequest request)
{
//If accessToken is null or expired, get a new one.
if (!HasValidToken())
{
RefreshAccessToken();
}
//request.AddOrUpdateParameter("Authorization", AccessToken, ParameterType.HttpHeader);
//var newCache = new CredentialCache
//{
// {new Uri("https://api.adp.com/"), "Bearer", new NetworkCredential(AccessToken, "") }
//};
var newCache = new CredentialCache();
newCache.Add(new Uri("https://api.adp.com/"), AdpAuthenticationModule.TheAuthenticationType, new NetworkCredential(AccessToken, ""));
request.Credentials = newCache;
//request.AddOrUpdateParameter("Authorization", "Bearer " + AccessToken, ParameterType.HttpHeader);
}
private void RefreshAccessToken()
{
try
{
var response = AuthenticationClient.Execute<AuthorizationResponse>(AuthenticationRequest);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
throw new FailedAuthenticationException($"Authentication failed to refresh access token, returned with code {response.StatusCode}. Content: \"{response.Content}\".", null);
}
if (string.IsNullOrWhiteSpace(response.Data.access_token))
{
throw new Exception("Error: response returned during access token refresh gave Status 200 OK, but access_token returned was null or whitespace.");
}
AccessToken = response.Data.access_token;
if (response.Data.expires_in <= 0)
{
throw new Exception("Error: response returned during access token refresh gave Status 200 OK, but expires_in value returned was <=0.");
}
TokenExpiresOn = DateTime.Now.AddSeconds(response.Data.expires_in);
}
catch (FailedAuthenticationException)
{
throw;
}
catch (Exception e)
{
throw new FailedAuthenticationException($"Authentication failed to refresh access token, see inner exception details.", e);
}
}
/// <summary>
/// Returns whether the current access token is valid.
/// </summary>
/// <returns>False if token is null or has 10 or less minutes until expiry; else returns true.</returns>
public bool HasValidToken()
{
return !string.IsNullOrEmpty(AccessToken) && DateTime.Now.CompareTo(TokenExpiresOn.AddMinutes(-10.0)) < 0;
}
private static AdpAuthenticationModule RegisterAuthenticationModule(Uri loginServerUrl)
{
var registeredModules = AuthenticationManager.RegisteredModules;
AdpAuthenticationModule authenticationModule;
while (registeredModules.MoveNext())
{
object current = registeredModules.Current;
if (current is AdpAuthenticationModule)
{
authenticationModule = (AdpAuthenticationModule)current;
if (authenticationModule.LoginServerUrl.Equals(loginServerUrl))
{
return authenticationModule;
}
}
}
authenticationModule = new AdpAuthenticationModule(loginServerUrl);
AuthenticationManager.Register(authenticationModule);
return authenticationModule;
}
}
My Custom Authentication Module:
public class AdpAuthenticationModule : IAuthenticationModule
{
/// <summary>
/// The name of the custom authentication type.
/// </summary>
public string AuthenticationType => TheAuthenticationType;
public static string TheAuthenticationType => "AdpAuthentication";
/// <summary>
/// Returns false, as this IAuthenticationModule cannot pre-authenticate.
/// </summary>
public bool CanPreAuthenticate => false;
private readonly CredentialCache credentialCache = new CredentialCache();
private readonly Uri loginServerUrl;
internal CredentialCache CredentialCache
{
get
{
return credentialCache;
}
}
internal Uri LoginServerUrl
{
get
{
return loginServerUrl;
}
}
internal AdpAuthenticationModule(Uri loginServerUrl)
{
this.loginServerUrl = loginServerUrl ?? throw new ArgumentNullException("AdpAuthenticationModule.loginServerUrl");
}
/// <summary>
/// Builds and returns a <see cref="Authorization"/> object for a request.
/// </summary>
/// <param name="challenge"></param>
/// <param name="request"></param>
/// <param name="credentials"></param>
/// <returns></returns>
public Authorization Authenticate(string challenge, WebRequest request, ICredentials credentials)
{
Authorization result = null;
if (request != null && credentials != null)
{
NetworkCredential creds = credentials.GetCredential(LoginServerUrl, AuthenticationType);
if (creds == null)
{
return null;
}
ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
if (policy != null && !policy.ShouldSendCredential(LoginServerUrl, request, creds, this))
{
return null;
}
string token = Convert.ToBase64String(Encoding.UTF8.GetBytes(creds.UserName));
result = new Authorization(string.Format("Bearer {0}", token));
}
return result;
}
/// <summary>
/// Returns null, since this IAuthenticationModule cannot pre-authenticate.
/// </summary>
/// <param name="request"></param>
/// <param name="credentials"></param>
/// <returns></returns>
public Authorization PreAuthenticate(WebRequest request, ICredentials credentials)
{
return null;
}
}
Implementation of IAuthenticationModule need to be registered in the AuthenticationManager class from System.Net.
Use the following code :
AuthenticationManager.Register(new AdpAuthenticationModule());
I am using the Visual Studio client tools for calling the VSTS REST APIs in a command line utility. This utility can be run several times for different commands (Copy, Delete, applying policies, etc.)
I'm creating the VssConnection like such
public static VssConnection CreateConnection(Uri url, VssCredentials credentials = null)
{
credentials = credentials ?? new VssClientCredentials();
credentials.Storage = new VssClientCredentialStorage();
var connection = new VssConnection(url, credentials);
connection.ConnectAsync().SyncResult();
return connection;
}
According to the docs, this should be caching the credentials so that you won't get prompted again when running my command line tool. But I get prompted every time I run my command line utility and the VssConnection tries to connect.
Is there anyway to cache the credentials so that the user won't be prompted every time they run the command line?
Should be noted that if I don't dispose the VssConnection, it will not prompt the next time I run it.
UPDATE
To be clear, the issue isn't caching the VssClientCredentials instance once the connection is created as that object is attached to the VssConnection object. The issue is caching the user token between execution of the program, i.e. on the local machine so that the next time the utility is executed from the command line the user doesn't have to once again type in their credentials. Similar to how you don't have to always log into Visual Studio each time you fire it up.
So I found a working solution that seems to be exactly what I wanted. If there is a better solution, please feel free to post.
Solution: Since VssClientCredentials.Storage property is expecting a class that implements IVssCredentialStorage, I created a class that implements that interface by deriving from the stock VssClientCredentialStorage class.
It then overrides the methods around retrieving and removing tokens to manage them based on an expiration lease which is stored with the token in the registry.
If the token is retrieved and has an expired lease, the token is removed from the storage and null is returned and the VssConnection class display a UI forcing the user to enter their credentials. If the token isn't expired, the user is not prompted and the cached credential is used.
So now I can do the following:
Call my utility from the command line for the first time
Supply credentials to the VSTS client prompt
Run the utility again from the command line without being prompted!
Now I've built into my utility a standard lease expiration but the user can alter it with a command line option. Also the user can clear the cached credentials as well.
The key is in the RemoveToken override. The call to the base class is what removes it from the registry, so if you bypass that (in my case if the lease hasn't expired) then the registry entry will remain. This allows the VssConnection to use the cached credentials and not prompt the user each time the program is executed!
Example of the calling code:
public static VssConnection CreateConnection(Uri url, VssCredentials credentials = null, double tokenLeaseInSeconds = VssClientCredentialCachingStorage.DefaultTokenLeaseInSeconds)
{
credentials = credentials ?? new VssClientCredentials();
credentials.Storage = GetVssClientCredentialStorage(tokenLeaseInSeconds);
var connection = new VssConnection(url, credentials);
connection.ConnectAsync().SyncResult();
return connection;
}
private static VssClientCredentialCachingStorage GetVssClientCredentialStorage(double tokenLeaseInSeconds)
{
return new VssClientCredentialCachingStorage("YourApp", "YourNamespace", tokenLeaseInSeconds);
}
The derived storage class:
/// <summary>
/// Class to alter the credential storage behavior to allow the token to be cached between sessions.
/// </summary>
/// <seealso cref="Microsoft.VisualStudio.Services.Common.IVssCredentialStorage" />
public class VssClientCredentialCachingStorage : VssClientCredentialStorage
{
#region [Private]
private const string __tokenExpirationKey = "ExpirationDateTime";
private double _tokenLeaseInSeconds;
#endregion [Private]
/// <summary>
/// The default token lease in seconds
/// </summary>
public const double DefaultTokenLeaseInSeconds = 86400;// one day
/// <summary>
/// Initializes a new instance of the <see cref="VssClientCredentialCachingStorage"/> class.
/// </summary>
/// <param name="storageKind">Kind of the storage.</param>
/// <param name="storageNamespace">The storage namespace.</param>
/// <param name="tokenLeaseInSeconds">The token lease in seconds.</param>
public VssClientCredentialCachingStorage(string storageKind = "VssApp", string storageNamespace = "VisualStudio", double tokenLeaseInSeconds = DefaultTokenLeaseInSeconds)
: base(storageKind, storageNamespace)
{
this._tokenLeaseInSeconds = tokenLeaseInSeconds;
}
/// <summary>
/// Removes the token.
/// </summary>
/// <param name="serverUrl">The server URL.</param>
/// <param name="token">The token.</param>
public override void RemoveToken(Uri serverUrl, IssuedToken token)
{
this.RemoveToken(serverUrl, token, false);
}
/// <summary>
/// Removes the token.
/// </summary>
/// <param name="serverUrl">The server URL.</param>
/// <param name="token">The token.</param>
/// <param name="force">if set to <c>true</c> force the removal of the token.</param>
public void RemoveToken(Uri serverUrl, IssuedToken token, bool force)
{
//////////////////////////////////////////////////////////
// Bypassing this allows the token to be stored in local
// cache. Token is removed if lease is expired.
if (force || token != null && this.IsTokenExpired(token))
base.RemoveToken(serverUrl, token);
//////////////////////////////////////////////////////////
}
/// <summary>
/// Retrieves the token.
/// </summary>
/// <param name="serverUrl">The server URL.</param>
/// <param name="credentialsType">Type of the credentials.</param>
/// <returns>The <see cref="IssuedToken"/></returns>
public override IssuedToken RetrieveToken(Uri serverUrl, VssCredentialsType credentialsType)
{
var token = base.RetrieveToken(serverUrl, credentialsType);
if (token != null)
{
bool expireToken = this.IsTokenExpired(token);
if (expireToken)
{
base.RemoveToken(serverUrl, token);
token = null;
}
else
{
// if retrieving the token before it is expired,
// refresh the lease period.
this.RefreshLeaseAndStoreToken(serverUrl, token);
token = base.RetrieveToken(serverUrl, credentialsType);
}
}
return token;
}
/// <summary>
/// Stores the token.
/// </summary>
/// <param name="serverUrl">The server URL.</param>
/// <param name="token">The token.</param>
public override void StoreToken(Uri serverUrl, IssuedToken token)
{
this.RefreshLeaseAndStoreToken(serverUrl, token);
}
/// <summary>
/// Clears all tokens.
/// </summary>
/// <param name="url">The URL.</param>
public void ClearAllTokens(Uri url = null)
{
IEnumerable<VssToken> tokens = this.TokenStorage.RetrieveAll(base.TokenKind).ToList();
if (url != default(Uri))
tokens = tokens.Where(t => StringComparer.InvariantCultureIgnoreCase.Compare(t.Resource, url.ToString().TrimEnd('/')) == 0);
foreach(var token in tokens)
this.TokenStorage.Remove(token);
}
private void RefreshLeaseAndStoreToken(Uri serverUrl, IssuedToken token)
{
if (token.Properties == null)
token.Properties = new Dictionary<string, string>();
token.Properties[__tokenExpirationKey] = JsonSerializer.SerializeObject(this.GetNewExpirationDateTime());
base.StoreToken(serverUrl, token);
}
private DateTime GetNewExpirationDateTime()
{
var now = DateTime.Now;
// Ensure we don't overflow the max DateTime value
var lease = Math.Min((DateTime.MaxValue - now.Add(TimeSpan.FromSeconds(1))).TotalSeconds, this._tokenLeaseInSeconds);
// ensure we don't have negative leases
lease = Math.Max(lease, 0);
return now.AddSeconds(lease);
}
private bool IsTokenExpired(IssuedToken token)
{
bool expireToken = true;
if (token != null && token.Properties.ContainsKey(__tokenExpirationKey))
{
string expirationDateTimeJson = token.Properties[__tokenExpirationKey];
try
{
DateTime expiration = JsonSerializer.DeserializeObject<DateTime>(expirationDateTimeJson);
expireToken = DateTime.Compare(DateTime.Now, expiration) >= 0;
}
catch { }
}
return expireToken;
}
}
Yesterday I coded myself a simple RESTful web API in .NET Core (solution named Vault) with a single method that gets the profile of the user depending only on the Windows user name. I now have a second solution that will call some requests to my self-hosting service previously mentioned. When I use Postman, I can retrieve the data with ease when I call a GET on my only method in Vault, but when I build the URI in Mainframe and execute, I get an Unauthorized error and it confuses me as Vault does not require specific login like username and password. I also put a breakpoint in Vault and unlike when I'm using Postman, it does not reach my code when calling via the Mainframe solution.
Here where is build my REST request and call the service (GetProfile())
public VaultApiClient(ConfigurationManagerWrap configuration)
{
this.configuration = configuration;
this.client = new RestClient(new Uri(this.configuration.GetAppSetting<string>(ConfigurationKeys.VaultApiURL)));
}
/// <summary>
/// The get profile.
/// </summary>
/// <returns>
/// The <see cref="UserProfile"/>.
/// </returns>
public UserProfile GetProfile()
{
var request = new RestRequest("profile") { Method = Method.GET};
//request.AddParameter("profile", ParameterType.UrlSegment);
var response = this.client.Execute(request);
if (response.StatusCode != HttpStatusCode.OK)
{
throw new Exception(
$"Could not get the user profile ({response.StatusCode} {response.StatusDescription})");
}
return RestJsonSerializer.Default.Deserialize<UserProfile>(response);
}
Im hosting local so the base URI, aka ConfigurationKeys.VaultApiURL, is localhost5000/api/
My Mainframe controller :
public HomeController()
: this(new VaultApiClient(new ConfigurationManagerWrap()))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HomeController"/> class.
/// </summary>
/// <param name="vaultApiClient">
/// The vault api client.
/// </param>
public HomeController(IVaultApiClient vaultApiClient)
{
this.vaultApiClient = vaultApiClient;
}
/// <summary>
/// The index.
/// </summary>
/// <returns>
/// The <see cref="ActionResult"/>.
/// </returns>
public ActionResult Index()
{
var profile = this.GetProfile();
this.ViewBag.IsEdit = false;
this.ViewBag.IsError = false;
this.ViewBag.ErrorMessage = "";
if (this.TempData.ContainsKey("IsEdit"))
{
this.ViewBag.IsEdit = true;
this.TempData.Remove("IsEdit");
if (this.TempData.ContainsKey("ErrorMessage"))
{
this.ViewBag.IsError = true;
this.ViewBag.ErrorMessage = this.TempData["ErrorMessage"];
this.TempData.Remove("ErrorMessage");
}
}
return this.View("Index", profile);
}
private UserProfile GetProfile()
{
return this.vaultApiClient.GetProfile();
}
And here is the vault controller method that handles the GET request in question:
[HttpGet]
[Route("/api/Profile")]
[Produces(typeof(UserProfile))]
public IActionResult SearchProfile()
{
try
{
if (!this.currentuser.IsAuthenticated)
{
throw new Exception("This service does not support anonymous calls.");
}
var profile = Task.Run(() => this.personalizationService.GetUserProfileAsync(this.currentuser.GetCurrentWindowsIdentityName)).Result;
var userProfile = this.persistenceToDataModelConverter.Convert(profile);
userProfile.UserAdLogin = this.currentuser.GetCurrentWindowsIdentityName;
return this.Ok(userProfile);
}
catch (Exception ex)
{
return this.NotFound(ex);
}
}
Lastly, here are a few pics of before and when the error is thrown.
Credential information must be provided with client request in order to authenticate with the server.
I'm stuck trying to access a specific Google drive account from a MVC app. All I need is for the MVC web app to access my google drive scan for a few files and alter the database based on the contents of the google drive. The problem is when running in IIS the drive cannot be authenticated as GoogleWebAuthorizationBroker tries to open browser if its a windows app but doesn't seem to be able to do that through IIS and even if it did it would be server side.
Ideally I would not have to authenticate this app at all, but if it has do go through that then how do I make it work in IIS?
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = "MY_ID",
ClientSecret = "My_Secret"
},
new[] { DriveService.Scope.Drive },
"user",
CancellationToken.None, dataStore: new FileDataStore(Server.MapPath("~/app_data/googledata"))).Result;
I got this to work, was able to enable the web site to access Google drive using my account without asking users to login or authorize.
First of all, follow this link to get Google API work with MVC:
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web_applications
There is a problem in the Sample code, in HomeController
public async Task IndexAsync(CancellationToken cancellationToken)
Should be:
public async Task<ActionResult> IndexAsync(CancellationToken cancellationToken)
After that, I created a MemoryDataStore (see code at the end) that is a slightly modification from the MemoryDataStore posted here:
http://conficient.wordpress.com/2014/06/18/using-google-drive-api-with-c-part-2/
Once you do that, capture the refresh token of the account you are using, and replace the store with this store when authenticate:
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = clientID,
ClientSecret = clientSecret
},
Scopes = new[] { DriveService.Scope.Drive },
//DataStore = new FileDataStore("Drive.Api.Auth.Store")
DataStore = new GDriveMemoryDataStore(commonUser, refreshToken)
});
Here commonUser is a predefined user id of your chosen. Please make sure to modify the GetUserID() method to return the same commonUser:
public override string GetUserId(Controller controller)
{
return commonUser;
}
Once this is done, Google drive will stop asking user to login and authorize the app.
Here is my MemoryDataStore code:
/// <summary>
/// Handles internal token storage, bypassing filesystem
/// </summary>
internal class GDriveMemoryDataStore : IDataStore
{
private Dictionary<string, TokenResponse> _store;
private Dictionary<string, string> _stringStore;
//private key password: notasecret
public GDriveMemoryDataStore()
{
_store = new Dictionary<string, TokenResponse>();
_stringStore = new Dictionary<string, string>();
}
public GDriveMemoryDataStore(string key, string refreshToken)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
if (string.IsNullOrEmpty(refreshToken))
throw new ArgumentNullException("refreshToken");
_store = new Dictionary<string, TokenResponse>();
// add new entry
StoreAsync<TokenResponse>(key,
new TokenResponse() { RefreshToken = refreshToken, TokenType = "Bearer" }).Wait();
}
/// <summary>
/// Remove all items
/// </summary>
/// <returns></returns>
public async Task ClearAsync()
{
await Task.Run(() =>
{
_store.Clear();
_stringStore.Clear();
});
}
/// <summary>
/// Remove single entry
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
public async Task DeleteAsync<T>(string key)
{
await Task.Run(() =>
{
// check type
AssertCorrectType<T>();
if (typeof(T) == typeof(string))
{
if (_stringStore.ContainsKey(key))
_stringStore.Remove(key);
}
else if (_store.ContainsKey(key))
{
_store.Remove(key);
}
});
}
/// <summary>
/// Obtain object
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
public async Task<T> GetAsync<T>(string key)
{
// check type
AssertCorrectType<T>();
if (typeof(T) == typeof(string))
{
if (_stringStore.ContainsKey(key))
return await Task.Run(() => { return (T)(object)_stringStore[key]; });
}
else if (_store.ContainsKey(key))
{
return await Task.Run(() => { return (T)(object)_store[key]; });
}
// key not found
return default(T);
}
/// <summary>
/// Add/update value for key/value
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
public Task StoreAsync<T>(string key, T value)
{
return Task.Run(() =>
{
if (typeof(T) == typeof(string))
{
if (_stringStore.ContainsKey(key))
_stringStore[key] = (string)(object)value;
else
_stringStore.Add(key, (string)(object)value);
} else
{
if (_store.ContainsKey(key))
_store[key] = (TokenResponse)(object)value;
else
_store.Add(key, (TokenResponse)(object)value);
}
});
}
/// <summary>
/// Validate we can store this type
/// </summary>
/// <typeparam name="T"></typeparam>
private void AssertCorrectType<T>()
{
if (typeof(T) != typeof(TokenResponse) && typeof(T) != typeof(string))
throw new NotImplementedException(typeof(T).ToString());
}
}
I'm not familiar with C#, but the generic OAuth answer is that you need to request a refresh token (once only and you can do this in the OAuth playground), then store/embed that token somewhere in your server so you can use it to request an access token whenever your server app needs to access Drive.
See How do I authorise an app (web or installed) without user intervention? (canonical ?) for details on how to do this. You will of course either need to reverse engineer how the C# library stores its tokens, or create/modify an equivalent which uses the manually obtained refresh token.
I suspect what you are looking for is a Service account. A service account will allow you to set up your application to access your google drive with out requiring you to Autenticate.
Google APIs also support Service accounts. Unlike the scenario in
which a client application requests access to an end-user's data,
service accounts provide access to the client application's own data.
You can find Google's documentation on how to implement a service account here. Google APIs Client Library for .NET : Service Account