Custom OAuth client in MVC4 / DotNetOpenAuth - missing access token secret - asp.net-mvc-4

I'm currently working on implementing a Dropbox OAuth client for my application. It's been a fairly painless process until I hit the end. Once I've authorized, when I attempt to access user data I get a 401 back from Dropbox about the token being invalid. I asked on the Dropbox forums and it looks like my request is missing the access_token_secret that Dropbox returns back. I was able to use Fiddler to dig out the secret and add it to my request url and it worked fine, so that's definitely the issue. So why doesn't DotNetOpenAuth return back the access token secret when it returns the access token?
For reference, my code:
public class DropboxClient : OAuthClient
{
public static readonly ServiceProviderDescription DropboxServiceDescription = new ServiceProviderDescription
{
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.dropbox.com/1/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new PlaintextSigningBindingElement() }
};
public DropboxClient(string consumerKey, string consumerSecret) :
this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}
public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) :
base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
}
protected override DotNetOpenAuth.AspNet.AuthenticationResult VerifyAuthenticationCore(DotNetOpenAuth.OAuth.Messages.AuthorizedTokenResponse response)
{
var profileEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/account/info", HttpDeliveryMethods.GetRequest);
HttpWebRequest request = this.WebWorker.PrepareAuthorizedRequest(profileEndpoint, response.AccessToken);
try
{
using (WebResponse profileResponse = request.GetResponse())
{
using (Stream profileResponseStream = profileResponse.GetResponseStream())
{
using (StreamReader reader = new StreamReader(profileResponseStream))
{
string jsonText = reader.ReadToEnd();
JavaScriptSerializer jss = new JavaScriptSerializer();
dynamic jsonData = jss.DeserializeObject(jsonText);
Dictionary<string, string> extraData = new Dictionary<string, string>();
extraData.Add("displayName", jsonData.display_name ?? "Unknown");
extraData.Add("userId", jsonData.uid ?? "Unknown");
return new DotNetOpenAuth.AspNet.AuthenticationResult(true, ProviderName, extraData["userId"], extraData["displayName"], extraData);
}
}
}
}
catch (WebException ex)
{
using (Stream s = ex.Response.GetResponseStream())
{
using (StreamReader sr = new StreamReader(s))
{
string body = sr.ReadToEnd();
return new DotNetOpenAuth.AspNet.AuthenticationResult(new Exception(body, ex));
}
}
}
}
}

I found your question when I was searching for solution to a similar problem. I solved it by making 2 new classes, which you can read about in this coderwall post.
I'll also copy and paste the full post here:
DotNetOpenAuth.AspNet 401 Unauthorized Error and Persistent Access Token Secret Fix
When designing QuietThyme, our Cloud Ebook Manager, we knew that everyone hates creating new accounts just as much as we do. We started looking for OAuth and OpenId libraries that we could leverage to allow for social login. We ended up using the DotNetOpenAuth.AspNet library for user authentication, because it supports Microsoft, Twitter, Facebook, LinkedIn and Yahoo, and many others right out of the bow. While we had some issues setting it all up, in the end we only needed to do a few small customizations to get most of it working (described in a previous coderwall post). We noticed that, unlike all the others, the LinkedIn client would not authenticate, returning a 401 Unauthorized Error from DotNetOpenAuth. It quickly became apparent that this was due to a signature issue, and after looking at the source we were able to determine that the retrieved AccessToken secret is not being used with the authenticated profile info request.
It acutally makes sense, the reason that OAuthClient class doesn't include the retrieved access token secret is that it's normally not needed for authentication purposes, which is the primary purpose of the ASP.NET OAuth library.
We needed to make authenticated requests against the api, after the user has logged in, to retrieve some standard profile information, including email address and full name. We were able to solve this issue by making use of an InMemoryOAuthTokenManager temporarily.
public class LinkedInCustomClient : OAuthClient
{
private static XDocument LoadXDocumentFromStream(Stream stream)
{
var settings = new XmlReaderSettings
{
MaxCharactersInDocument = 65536L
};
return XDocument.Load(XmlReader.Create(stream, settings));
}
/// Describes the OAuth service provider endpoints for LinkedIn.
private static readonly ServiceProviderDescription LinkedInServiceDescription =
new ServiceProviderDescription
{
AccessTokenEndpoint =
new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/accessToken",
HttpDeliveryMethods.PostRequest),
RequestTokenEndpoint =
new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/requestToken?scope=r_basicprofile+r_emailaddress",
HttpDeliveryMethods.PostRequest),
UserAuthorizationEndpoint =
new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authorize",
HttpDeliveryMethods.PostRequest),
TamperProtectionElements =
new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
//ProtocolVersion = ProtocolVersion.V10a
};
private string ConsumerKey { get; set; }
private string ConsumerSecret { get; set; }
public LinkedInCustomClient(string consumerKey, string consumerSecret)
: this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { }
public LinkedInCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
: base("linkedIn", LinkedInServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
ConsumerKey = consumerKey;
ConsumerSecret = consumerSecret;
}
//public LinkedInCustomClient(string consumerKey, string consumerSecret) :
// base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret) { }
/// Check if authentication succeeded after user is redirected back from the service provider.
/// The response token returned from service provider authentication result.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification = "We don't care if the request fails.")]
protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response)
{
// See here for Field Selectors API http://developer.linkedin.com/docs/DOC-1014
const string profileRequestUrl =
"https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary,email-address)";
string accessToken = response.AccessToken;
var profileEndpoint =
new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest);
try
{
InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);
HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);
using (WebResponse profileResponse = request.GetResponse())
{
using (Stream responseStream = profileResponse.GetResponseStream())
{
XDocument document = LoadXDocumentFromStream(responseStream);
string userId = document.Root.Element("id").Value;
string firstName = document.Root.Element("first-name").Value;
string lastName = document.Root.Element("last-name").Value;
string userName = firstName + " " + lastName;
string email = String.Empty;
try
{
email = document.Root.Element("email-address").Value;
}
catch(Exception)
{
}
var extraData = new Dictionary<string, string>();
extraData.Add("accesstoken", accessToken);
extraData.Add("name", userName);
extraData.AddDataIfNotEmpty(document, "headline");
extraData.AddDataIfNotEmpty(document, "summary");
extraData.AddDataIfNotEmpty(document, "industry");
if(!String.IsNullOrEmpty(email))
{
extraData.Add("email",email);
}
return new AuthenticationResult(
isSuccessful: true, provider: this.ProviderName, providerUserId: userId, userName: userName, extraData: extraData);
}
}
}
catch (Exception exception)
{
return new AuthenticationResult(exception);
}
}
}
Here's the section that has changed from the base LinkedIn client written by Microsoft.
InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);
HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);
Unfortunately, the IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) method does not get executed until after the VerifyAuthentication() method returns, so we instead have to create a new TokenManager and and create a WebConsumer and HttpWebRequest using the AccessToken credentials we just retrieved.
This solves our simple 401 Unauthorized issue.
Now what happens if you would like to persist the AccessToken credentials after the authentication process? This could be useful for a DropBox client for instance, where you would like to sync files to a user's DropBox asyncronously. The issue goes back to the way the AspNet library was written, it was assumed that DotNetOpenAuth would only be used for user authethentication, not as a basis for futher OAuth api calls. Thankfully the fix was fairly simple, all I had to do was modify the base AuthetnicationOnlyCookieOAuthTokenManger so that the ReplaceRequestTokenWithAccessToken(..) method stored the new AccessToken key and secrets.
/// <summary>
/// Stores OAuth tokens in the current request's cookie
/// </summary>
public class PersistentCookieOAuthTokenManagerCustom : AuthenticationOnlyCookieOAuthTokenManager
{
/// <summary>
/// Key used for token cookie
/// </summary>
private const string TokenCookieKey = "OAuthTokenSecret";
/// <summary>
/// Primary request context.
/// </summary>
private readonly HttpContextBase primaryContext;
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
/// </summary>
public PersistentCookieOAuthTokenManagerCustom() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
/// </summary>
/// <param name="context">The current request context.</param>
public PersistentCookieOAuthTokenManagerCustom(HttpContextBase context) : base(context)
{
this.primaryContext = context;
}
/// <summary>
/// Gets the effective HttpContext object to use.
/// </summary>
private HttpContextBase Context
{
get
{
return this.primaryContext ?? new HttpContextWrapper(HttpContext.Current);
}
}
/// <summary>
/// Replaces the request token with access token.
/// </summary>
/// <param name="requestToken">The request token.</param>
/// <param name="accessToken">The access token.</param>
/// <param name="accessTokenSecret">The access token secret.</param>
public new void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret)
{
//remove old requestToken Cookie
//var cookie = new HttpCookie(TokenCookieKey)
//{
// Value = string.Empty,
// Expires = DateTime.UtcNow.AddDays(-5)
//};
//this.Context.Response.Cookies.Set(cookie);
//Add new AccessToken + secret Cookie
StoreRequestToken(accessToken, accessTokenSecret);
}
}
Then to use this PersistentCookieOAuthTokenManager all you need to do is modify your DropboxClient constructor, or any other client where you would like to persist the AccessToken Secret
public DropBoxCustomClient(string consumerKey, string consumerSecret)
: this(consumerKey, consumerSecret, new PersistentCookieOAuthTokenManager()) { }
public DropBoxCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
: base("dropBox", DropBoxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{}

The reason that OAuthClient class doesn't include access token secret is that it's normally not needed for authentication purpose, which is the primary purpose of the ASP.NET OAuth library.
That said, if you want to retrieve the access token secret in your case, you can override the VerifyAuthentication() method, instead of VerifyAuthenticationCore() like you are doing above. Inside VerifyAuthentication(), you can call WebWorker.ProcessUserAuthorization() to validation the login and from the returned AuthorizedTokenResponse object, you have access to the token secret.

After doing some digging, I was able to solve this by changing my constructor logic as follows:
public DropboxClient(string consumerKey, string consumerSecret) :
this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}
public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) :
base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
}
becomes
public DropboxClient(string consumerKey, string consumerSecret) :
base("dropbox", DropboxServiceDescription, consumerKey, consumerSecret)
{
}
Digging through the DNOA source shows that if you construct an OAuthClient (my base class) with just the consumer key and secret, it uses the InMemoryOAuthTokenManager instead of the SimpleConsumerTokenManager. I don't know why, but now my access token secret is properly appended to my signature in the authorized request and everything works. Hopefully this helps someone else. In the meantime, I'll likely clean this up for a blog post since there is zero guidance on the net (that I can find) for doing this.
EDIT: I'm going to undo my answer since, as a colleague pointed out, this will take care of one request, but now that I'm using the in-memory manager, that will flush once I round trip fully back to the browser (I'm assuming). So I think the root issue here is that I need to get the access token secret, which I still haven't seen how to do.

As for your original question that the secret is not provided in response--the secret is right there when you get the response in the verifyAuthenticationCore function. You get both of them like this:
string token = response.AccessToken; ;
string secret = (response as ITokenSecretContainingMessage).TokenSecret;

Related

IdentityServer4 losing original returnUrl when using External Login server

Unfortunately, given the size of the project, I can’t easily share a reproducible version. However, hopefully what I have below will shed some light on my issue and you’ll see where I made a mistake.
I have two sites, an ASP.Net Core MVC application and a Login server, also ASP.Net Core MVC. Let’s call them http://mvc.mysite.com and http://login.mysite.com. Neither are significantly different from the IdentityServer4 Quickstart #6. The only real difference is that I have implemented an external login provider for AzureAd. My code for that is below.
Scenario 1
Given an internal login flow, where the user uses an internal login page at http://login.mysite.com everything works fine.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login
User logs in with correct username/password
User is redirected to http://mvc.mysite.com/clients/client-page-1
Scenario 2
However, if the login server’s AccountController::Login() method determines there is a single ExternalLoginProvider and executes the line “return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);” then the original redirectUrl is lost.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login (receiving the output of AccountController::ExternalLogin)
User is redirected to AzureAd External OIDC Provider
User logs in with correct username/password
User is redirected to http://login.mysite.com/Account/ExternalLoginCallback
User is redirected to http://mvc.mysite.com (Notice that the user is redirected to the root of the MVC site instead of /clients/client-page-1)
For Scenario 1:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com/clients/client-page-1”
For Scenario 2:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com” (missing the /client.client-page-1)
In my login server’s Startup.cs I have added this to ConfigureServices:
services.AddAuthentication()
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
AzureAdOptions.Settings = options;
});
The implementation of AddAzureAd is as follows: (You’ll see options objects handed around, I have replaced all uses of options with constant values except for ClientId and ClientSecret).
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.AddOpenIdConnect("AzureAd", "Azure AD", options =>
{
var opts = new AzureAdOptions();
configureOptions(opts);
var config = new ConfigureAzureOptions(opts);
config.Configure(options);
});
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(AzureAdOptions azureOptions)
{
_azureOptions = azureOptions;
}
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions) : this(azureOptions.Value) {}
public void Configure(string name, OpenIdConnectOptions options)
{
Configure(options);
}
public void Configure(OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = "https://login.microsoftonline.com/common"; //_azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = "/signin-oidc"; // _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false; // true in production // _azureOptions.RequireHttps;
options.ClientSecret = _azureOptions.ClientSecret;
// Add code for hybridflow
options.ResponseType = "id_token code";
options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
};
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
/// <summary>
/// Redeems the authorization code by calling AcquireTokenByAuthorizationCodeAsync in order to ensure
/// that the cache has a token for the signed-in user.
/// </summary>
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
}
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
throw context.Exception;
}
}
}
public class NaiveSessionCache : TokenCache
{
private static readonly object FileLock = new object();
string UserObjectId = string.Empty;
string CacheId = string.Empty;
ISession Session = null;
public NaiveSessionCache(string userId, ISession session)
{
UserObjectId = userId;
CacheId = UserObjectId + "_TokenCache";
Session = session;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
Load();
}
public void Load()
{
lock (FileLock)
this.Deserialize(Session.Get(CacheId));
}
public void Persist()
{
lock (FileLock)
{
// reflect changes in the persistent store
Session.Set(CacheId, this.Serialize());
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
Session.Remove(CacheId);
}
public override void DeleteItem(TokenCacheItem item)
{
base.DeleteItem(item);
Persist();
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Load();
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
Persist();
}
}

AuthenticationHandler error AuthenticationScheme: Bearer was forbidden

I'm making a small service using ASP.NET Core. The first complicated thing I'm facing now is to authenticate an user into my system.
Let me introduce about my authentication flow:
+)Client -> call (api/account/authorize) -> System check whether client is valid or not -> Send token back to client as him/her is valid.
+) Client -> uses the obtained token -> requests to api/account/filter -> Service validates the token and throw the information back.
I've read some tutorials about JWT, but the response doesn't include enough information as I need. I want :
Throw 401 and a message describes that status code , i.e: "ACCOUNT_DISABLED", "ACCOUNT_PENDING", "ACCOUNT_PERMISSION_INSUFFICIENT", ... not just 401.
Therfore, I implemented my own Authenticate validator:
public class BearerAuthenticationHandler : AuthenticationHandler<BearerAuthenticationOption>
{
#region Properties
/// <summary>
/// Inject dependency service into the handler.
/// </summary>
private readonly JwtTokenSetting _encryptionSetting;
/// <summary>
/// Inject dependency service into the handler.
/// </summary>
private readonly IEncryptionService _encryptionService;
/// <summary>
/// Inject time service to handler.
/// </summary>
private readonly ITimeService _timeService;
private readonly IRepositoryAccount _repositoryAccount;
#endregion
#region Constructors
/// <summary>
/// Initialize an instance of handler with specific dependency injections.
/// </summary>
/// <param name="encryptionSetting"></param>
/// <param name="encryptionService"></param>
/// <param name="timeService"></param>
/// <param name="repositoryAccount"></param>
public BearerAuthenticationHandler(JwtTokenSetting encryptionSetting, IEncryptionService encryptionService, ITimeService timeService, IRepositoryAccount repositoryAccount)
{
_encryptionSetting = encryptionSetting;
_encryptionService = encryptionService;
_timeService = timeService;
_repositoryAccount = repositoryAccount;
}
#endregion
#region Methods
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
#region Token analyzation
// Find the authorization key in request.
var authorizationKey =
Request.Headers.Keys.FirstOrDefault(x => x.Equals("authorization", StringComparison.OrdinalIgnoreCase));
// Authorization key is not found in the request.
if (string.IsNullOrWhiteSpace(authorizationKey))
return AuthenticateResult.Fail("No authorization is found in request header.");
// Find the token in Authorization.
var authorizationValue = Request.Headers[authorizationKey].ToString();
// Authentication scheme prefix.
var authenticationScheme = $"{Options.AuthenticationScheme} ";
// No token has been specified.
if (string.IsNullOrWhiteSpace(authorizationValue) || !authorizationValue.StartsWith(authenticationScheme, StringComparison.OrdinalIgnoreCase))
return AuthenticateResult.Fail("No bearer token is found in request header.");
// Cut the string to obtain bearer token.
var accessToken = authorizationValue.Substring(authenticationScheme.Length);
#endregion
#region Token validation
// Decrypt the token.
var tokenDetailViewModel = _encryptionService.Decrypt<TokenDetailViewModel>(accessToken, _encryptionSetting.Key);
// No detail has been found.
if (tokenDetailViewModel == null)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "TOKEN_INVALID"
});
return AuthenticateResult.Fail("Token is invalid");
}
// Find the current unix time on server.
var unixTime = _timeService.UtcToUnix(DateTime.UtcNow);
// Token is expired.
if (unixTime > tokenDetailViewModel.Expire)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "TOKEN_EXPIRED"
});
return AuthenticateResult.Fail("Token is expired");
}
// Account filter construction.
var filterAccountViewModel = new FilterAccountViewModel
{
Email = tokenDetailViewModel.Email,
EmailComparison = TextComparision.Equal,
Password = tokenDetailViewModel.Password,
PasswordComparision = TextComparision.EqualIgnoreCase,
Statuses = new[] { AccountStatus.Active }
};
// Find the first condition statisfied account in the database.
var account = await _repositoryAccount.FindAccountAsync(filterAccountViewModel);
// Account cannot be found in the database.
if (account == null)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "ACCOUNT_INVALID"
});
return AuthenticateResult.Fail("Account is invalid");
}
#endregion
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(new Claim(nameof(JwtClaim.Email), account.Email));
claimsIdentity.AddClaim(new Claim(nameof(JwtClaim.Status), nameof(account.Status)));
// Update user into context.
var claimPrincipal = new ClaimsPrincipal(claimsIdentity);
// Initialize an authentication ticket.
var authenticationTicket = new AuthenticationTicket(claimPrincipal, new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
IsPersistent = true,
IssuedUtc = DateTime.UtcNow
}, "Bearer");
return AuthenticateResult.Success(authenticationTicket);
}
/// <summary>
/// Initialize an application/json response.
/// </summary>
/// <param name="httpResponse"></param>
/// <param name="httpStatusCode"></param>
/// <param name="httpResponseViewModel"></param>
private void InitializeHttpResponse(HttpResponse httpResponse, HttpStatusCode httpStatusCode, HttpResponseViewModel httpResponseViewModel)
{
// Response must be always application/json.
httpResponse.ContentType = "application/json";
httpResponse.StatusCode = (int)httpStatusCode;
if (httpResponseViewModel == null)
return;
using (var streamWriter = new StreamWriter(httpResponse.Body))
{
streamWriter.AutoFlush = true;
streamWriter.WriteLineAsync(JsonConvert.SerializeObject(httpResponseViewModel));
}
}
#endregion
}
Here is my AccountController:
[Route("api/[controller]")]
public class AccountController : Controller
{
private readonly IRepositoryAccount _repositoryAccount;
private readonly IEncryptionService _encryptionService;
private readonly ITimeService _timeService;
private readonly JwtTokenSetting _jwtTokenSetting;
public AccountController(IRepositoryAccount repositoryAccount, IEncryptionService encryptionService, ITimeService timeService,
IOptions<JwtTokenSetting> jwtTokenSetting)
{
_repositoryAccount = repositoryAccount;
_encryptionService = encryptionService;
_timeService = timeService;
_jwtTokenSetting = jwtTokenSetting.Value;
}
[HttpPost("authorize")]
[AllowAnonymous]
public async Task<IActionResult> Authorize([FromBody] LoginViewModel loginViewModel)
{
// Find the encrypted password of login information.
var filterAccountViewModel = new FilterAccountViewModel();
filterAccountViewModel.Email = loginViewModel.Email;
filterAccountViewModel.EmailComparison = TextComparision.Equal;
filterAccountViewModel.Password = _encryptionService.FindEncryptPassword(loginViewModel.Password);
filterAccountViewModel.PasswordComparision = TextComparision.EqualIgnoreCase;
filterAccountViewModel.Statuses = new[] {AccountStatus.Active};
// Initialize HttpResponseViewModel.
var httpResponseViewModel = new HttpResponseViewModel();
// Find the account.
var account = await _repositoryAccount.FindAccountAsync(filterAccountViewModel);
// Account is not found.
if (account == null)
{
Response.ContentType = "application/json";
using (var streamWriter = new StreamWriter(Response.Body))
{
httpResponseViewModel.Message = "ACCOUNT_INVALID";
await streamWriter.WriteLineAsync(JsonConvert.SerializeObject(httpResponseViewModel));
}
return new UnauthorizedResult();
}
// Initialize token detail.
var tokenDetailViewModel = new TokenDetailViewModel
{
Email = loginViewModel.Email,
Password = filterAccountViewModel.Password,
Expire = _timeService.UtcToUnix(DateTime.UtcNow.AddSeconds(_jwtTokenSetting.Expire))
};
// Initialize token information and throw to client for their future use.
var tokenGeneralViewModel = new TokenGeneralViewModel
{
AccessToken = _encryptionService.Encrypt(tokenDetailViewModel, _jwtTokenSetting.Key),
Expire = _jwtTokenSetting.Expire
};
return Ok(tokenGeneralViewModel);
}
[HttpPost("filter")]
[Authorize(ActiveAuthenticationSchemes = "Bearer")]
public IEnumerable<string> FindAllAccounts()
{
Response.StatusCode = (int)HttpStatusCode.Accepted;
return new[] { "1", "2", "3", "4" };
}
}
When I use the token generated by api/account/authorize to access api/account/filter. An error was thrown to me :
AuthenticationScheme: Bearer was forbidden
Can anyone please tell me why ? Is my implementation the best approach or not ?
Thank you,
Is my implementation the best approach or not ?
I wouldn't do this as you implemented. Because(1 and 3 are just my opinions)
ACCOUNT_DISABLED, ACCOUNT_PENDING,
ACCOUNT_PERMISSION_INSUFFICIENT this statuses doesn't mean that user has to
retype its cridentials.
Even if i want to use 401 with a message, before creating my own
handler implementation, i would consider using jwt bearer events. OnChallenge event seems good to do this(See this answer how to implement).
I think your requirement is related with authorization rather than authentication. So writing a policy
would be better.
To use policy i don't know simple implementation, but here is my attempt:
Authorization Handler:
public class CheckUserRequirement : IAuthorizationRequirement
{
}
public class CheckUserAuthorizationHandler : AuthorizationHandler<CheckUserRequirement>
{
private readonly IHttpContextAccessor _accessor;
public SimpleAuthorizationHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, SimpleRequirement requirement)
{
if(account.isDisabled)
{
_accessor.HttpContext.Response.Headers.Add("error_code", "ACCOUNT_DISABLED");
}
//...
context.Succeed(requirement);
}
}
ConfigureServices:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthorizationHandler, CheckUserAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("CheckUser", policy => { policy.AddRequirements(new CheckUserRequirement()); });
});
And use it:
[Authorize(Policy = "CheckUser")]
public class SomeController
Edit
I had suggested OnChallenge event, but i realized that it is not suitable for your case. See my another answer

Basic Authentication in ASP.NET Core

Question
How can I implement Basic Authentication with Custom Membership in an ASP.NET Core web application?
Notes
In MVC 5 I was using the instructions in this article which requires adding a module in the WebConfig.
I am still deploying my new MVC Coreapplication on IIS but this approach seems not working.
I also do not want to use the IIS built in support for Basic authentication, since it uses Windows credentials.
ASP.NET Security will not include Basic Authentication middleware due to its potential insecurity and performance problems.
If you require Basic Authentication middleware for testing purposes, then please look at https://github.com/blowdart/idunno.Authentication
ASP.NET Core 2.0 introduced breaking changes to Authentication and Identity.
On 1.x auth providers were configured via Middleware (as the accepted answer's implementation).
On 2.0 it's based on services.
Details on MS doc:
https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x
I've written a Basic Authentication implementation for ASP.NET Core 2.0 and publish to NuGet:
https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic
I'm disappointed by the ASP.NET Core authentication middleware design. As a framework it should simplify and led to greater productivity which isn't the case here.
Anyway, a simple yet secure approach is based on the Authorization filters e.g. IAsyncAuthorizationFilter. Note that an authorization filter will be executed after the other middlewares, when MVC picks a certain controller action and moves to filter processing. But within filters, authorization filters are executed first (details).
I was just going to comment on Clays comment to Hector's answer but didn't like Hectors example throwing exceptions and not having any challenge mechanism, so here is a working example.
Keep in mind:
Basic authentication without HTTPS in production is extremely bad. Make sure your HTTPS settings are hardened (e.g. disable all SSL and TLS < 1.2 etc.)
Today, most usage of basic authentication is when exposing an API that's protected by an API key (see Stripe.NET, Mailchimp etc). Makes for curl friendly APIs that are as secure as the HTTPS settings on the server.
With that in mind, don't buy into any of the FUD around basic authentication. Skipping something as basic as basic authentication is high on opinion and low on substance. You can see the frustration around this design in the comments here.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace BasicAuthFilterDemo
{
public class BasicAuthenticationFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Realm { get; set; }
public const string AuthTypeName = "Basic ";
private const string _authHeaderName = "Authorization";
public BasicAuthenticationFilterAttribute(string realm = null)
{
Realm = realm;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
try
{
var request = context?.HttpContext?.Request;
var authHeader = request.Headers.Keys.Contains(_authHeaderName) ? request.Headers[_authHeaderName].First() : null;
string encodedAuth = (authHeader != null && authHeader.StartsWith(AuthTypeName)) ? authHeader.Substring(AuthTypeName.Length).Trim() : null;
if (string.IsNullOrEmpty(encodedAuth))
{
context.Result = new BasicAuthChallengeResult(Realm);
return;
}
var (username, password) = DecodeUserIdAndPassword(encodedAuth);
// Authenticate credentials against database
var db = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));
var userManager = (UserManager<User>)context.HttpContext.RequestServices.GetService(typeof(UserManager<User>));
var founduser = await db.Users.Where(u => u.Email == username).FirstOrDefaultAsync();
if (!await userManager.CheckPasswordAsync(founduser, password))
{
// writing to the Result property aborts rest of the pipeline
// see https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.0#cancellation-and-short-circuiting
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
// Populate user: adjust claims as needed
var claims = new[] { new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, AuthTypeName) };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthTypeName));
context.HttpContext.User = principal;
}
catch
{
// log and reject
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
}
private static (string userid, string password) DecodeUserIdAndPassword(string encodedAuth)
{
var userpass = Encoding.UTF8.GetString(Convert.FromBase64String(encodedAuth));
var separator = userpass.IndexOf(':');
if (separator == -1)
return (null, null);
return (userpass.Substring(0, separator), userpass.Substring(separator + 1));
}
}
}
And these are the supporting classes
public class StatusCodeOnlyResult : ActionResult
{
protected int StatusCode;
public StatusCodeOnlyResult(int statusCode)
{
StatusCode = statusCode;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
return base.ExecuteResultAsync(context);
}
}
public class BasicAuthChallengeResult : StatusCodeOnlyResult
{
private string _realm;
public BasicAuthChallengeResult(string realm = "") : base(StatusCodes.Status401Unauthorized)
{
_realm = realm;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
context.HttpContext.Response.Headers.Add("WWW-Authenticate", $"{BasicAuthenticationFilterAttribute.AuthTypeName} Realm=\"{_realm}\"");
return base.ExecuteResultAsync(context);
}
}
We implemented Digest security for an internal service by using an ActionFilter:
public class DigestAuthenticationFilterAttribute : ActionFilterAttribute
{
private const string AUTH_HEADER_NAME = "Authorization";
private const string AUTH_METHOD_NAME = "Digest ";
private AuthenticationSettings _settings;
public DigestAuthenticationFilterAttribute(IOptions<AuthenticationSettings> settings)
{
_settings = settings.Value;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
ValidateSecureChannel(context?.HttpContext?.Request);
ValidateAuthenticationHeaders(context?.HttpContext?.Request);
base.OnActionExecuting(context);
}
private void ValidateSecureChannel(HttpRequest request)
{
if (_settings.RequireSSL && !request.IsHttps)
{
throw new AuthenticationException("This service must be called using HTTPS");
}
}
private void ValidateAuthenticationHeaders(HttpRequest request)
{
string authHeader = GetRequestAuthorizationHeaderValue(request);
string digest = (authHeader != null && authHeader.StartsWith(AUTH_METHOD_NAME)) ? authHeader.Substring(AUTH_METHOD_NAME.Length) : null;
if (string.IsNullOrEmpty(digest))
{
throw new AuthenticationException("You must send your credentials using Authorization header");
}
if (digest != CalculateSHA1($"{_settings.UserName}:{_settings.Password}"))
{
throw new AuthenticationException("Invalid credentials");
}
}
private string GetRequestAuthorizationHeaderValue(HttpRequest request)
{
return request.Headers.Keys.Contains(AUTH_HEADER_NAME) ? request.Headers[AUTH_HEADER_NAME].First() : null;
}
public static string CalculateSHA1(string text)
{
var sha1 = System.Security.Cryptography.SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(text));
return Convert.ToBase64String(hash);
}
}
Afterwards you can annotate the controllers or methods you want to be accessed with Digest security:
[Route("api/xxxx")]
[ServiceFilter(typeof(DigestAuthenticationFilterAttribute))]
public class MyController : Controller
{
[HttpGet]
public string Get()
{
return "HELLO";
}
}
To implement Basic security, simply change the DigestAuthenticationFilterAttribute to not use SHA1 but direct Base64 decoding of the Authorization header.
Super-Simple Basic Authentication in .NET Core:
1. Add this utility method:
static System.Text.Encoding ISO_8859_1_ENCODING = System.Text.Encoding.GetEncoding("ISO-8859-1");
public static (string, string) GetUsernameAndPasswordFromAuthorizeHeader(string authorizeHeader)
{
if (authorizeHeader == null || !authorizeHeader.Contains("Basic "))
return (null, null);
string encodedUsernamePassword = authorizeHeader.Substring("Basic ".Length).Trim();
string usernamePassword = ISO_8859_1_ENCODING.GetString(Convert.FromBase64String(encodedUsernamePassword));
string username = usernamePassword.Split(':')[0];
string password = usernamePassword.Split(':')[1];
return (username, password);
}
2. Update controller action to get username and password from Authorization header:
public async Task<IActionResult> Index([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
// Now use username and password with whatever authentication process you want
return View();
}
Example
This example demonstrates using this with ASP.NET Core Identity.
public class HomeController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
public HomeController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[AllowAnonymous]
public async Task<IActionResult> MyApiEndpoint([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
IdentityUser user = await _userManager.FindByNameAsync(username);
bool successfulAuthentication = await _userManager.CheckPasswordAsync(user, password);
if (successfulAuthentication)
return Ok();
else
return Unauthorized();
}
}

How are bearer tokens stored server-side in Web API 2?

I am setting up bearer token authentication in Web API 2, and I don't understand how (or where) the bearer token is being stored server-side. Here is the relevant code:
Startup:
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static Func<UserManager<IdentityUser>> UserManagerFactory { get; set; }
public static string PublicClientId { get; private set; }
static Startup()
{
PublicClientId = "self";
UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>());
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
}
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseOAuthBearerTokens(OAuthOptions);
}
}
WebApiConfig:
public class WebApiConfig
{
public static void ConfigureWebApi()
{
Register(GlobalConfiguration.Configuration);
}
public static void Register(HttpConfiguration http)
{
AuthUtil.ConfigureWebApiToUseOnlyBearerTokenAuthentication(http);
http.Routes.MapHttpRoute("ActionApi", "api/{controller}/{action}", new {action = Actions.Default});
}
}
AuthUtil:
public class AuthUtil
{
public static string Token(string email)
{
var identity = new ClaimsIdentity(Startup.OAuthOptions.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, email));
var ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
var currentUtc = new SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromMinutes(30));
var token = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
return token;
}
public static void ConfigureWebApiToUseOnlyBearerTokenAuthentication(HttpConfiguration http)
{
http.SuppressDefaultHostAuthentication();
http.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
}
}
LoginController:
public class LoginController : ApiController
{
...
public HttpResponseMessage Post([FromBody] LoginJson loginJson)
{
HttpResponseMessage loginResponse;
if (/* is valid login */)
{
var accessToken = AuthUtil.Token(loginJson.email);
loginResponse = /* HTTP response including accessToken */;
}
else
{
loginResponse = /* HTTP response with error */;
}
return loginResponse;
}
}
Using the above code, I'm able to login and store the bearer token client-side in a cookie, and then make calls to controllers marked with [Authorize] and it lets me in.
My questions are:
Where / how is the bearer token being stored server-side? It seems like this is hapenning through one of the OWIN calls but I can't tell where.
Is it possible to persist the bearer tokens to a database server-side so that they can remain in place after a Web API server restart?
If the answer to #2 is no, is there anyway for a client to maintain its bearer token and re-use it even after the Web API goes down and comes back up? While this may be rare in Production, it can happen quite often doing local testing.
They're not stored server side -- they're issued to the client and the client presents them on each call. They're verified because they're signed by the owin host's protection key. In SystemWeb hosting, that protection key is the machineKey setting from web.config.
That's unnecessary, as long as the protection key the owin host uses doesn't change across server restarts.
A client can hold onto a token for as long as the token is valid.
For those who are looking for how to set web.config, here is a sample
<system.web>
<machineKey validation="HMACSHA256" validationKey="64-hex"
decryption="AES" decryptionKey="another-64-hex"/>
</system.web>
You need both validationKey and decriptionkey to make it work.
And here is how to generate keys
https://msdn.microsoft.com/en-us/library/ms998288.aspx
To add to this, the token can be persisted server side using the SessionStore property of of CookieAuthenticationOptions. I wouldn't advocate doing this but it's there if your tokens become excessively large.
This is an IAuthenticationSessionStore so you could implement your own storage medium.
By default the token is not stored by the server. Only your client has it and is sending it through the authorization header to the server.
If you used the default template provided by Visual Studio, in the Startup ConfigureAuth method the following IAppBuilder extension is called: app.UseOAuthBearerTokens(OAuthOptions).

How to implement three legged OAuth with MVC 4?

I'm trying to use ASP .NET MVC 4's built in OAuth functionality to connect to QuickBooks. The problem (I mean, uh challenge) is that the built in templates (facebook, google, etc.) all offer two legged authorization. QuickBooks uses three. As a result, this has really been stomping a mud hole in my butt.
The good news is that QuickBooks provides an MVC 3 example. You may find it here:
https://ipp.developer.intuit.com/0010_Intuit_Partner_Platform/0025_Intuit_Anywhere/0020_Connect/0010_From_Within_Your_App/Implement_OAuth_in_Your_App
The most relevant code that's causing me problems is here:
private String consumerSecret, consumerKey, oauthLink, RequestToken, TokenSecret, oauth_callback_url;
/// <summary>
/// Action Result for Index, This flow will create OAuthConsumer Context using Consumer key and Consuler Secret key
/// obtained when Application is added at intuit workspace. It creates OAuth Session out of OAuthConsumer and Calls
/// Intuit Workpsace endpoint for OAuth.
/// </summary>
/// <returns>Redirect Result.</returns>
public RedirectResult Index()
{
oauth_callback_url = Request.Url.GetLeftPart(UriPartial.Authority) + ConfigurationManager.AppSettings["oauth_callback_url"];
consumerKey = ConfigurationManager.AppSettings["consumerKey"];
consumerSecret = ConfigurationManager.AppSettings["consumerSecret"];
oauthLink = Constants.OauthEndPoints.IdFedOAuthBaseUrl;
IToken token = (IToken)Session["requestToken"];
IOAuthSession session = CreateSession();
IToken requestToken = session.GetRequestToken();
Session["requestToken"] = requestToken;
RequestToken = requestToken.Token;
TokenSecret = requestToken.TokenSecret;
oauthLink = Constants.OauthEndPoints.AuthorizeUrl + "?oauth_token=" + RequestToken + "&oauth_callback=" + UriUtility.UrlEncode(oauth_callback_url);
return Redirect(oauthLink);
}
/// <summary>
/// Gets the Access Token
/// </summary>
/// <returns>Returns OAuth Session</returns>
protected IOAuthSession CreateSession()
{
OAuthConsumerContext consumerContext = new OAuthConsumerContext
{
ConsumerKey = consumerKey,
ConsumerSecret = consumerSecret,
SignatureMethod = SignatureMethod.HmacSha1
};
OAuthSession s = new OAuthSession(consumerContext,
Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlRequestToken,
oauthLink,
Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlAccessToken);
return s;
}
From that, you can see that the MVC 3 example creates a session and grabs a request token. My immediate problem is locating the bit of code in MVC 4 where I can retrieve this request token. I think it is in my AccountController here:
//
// POST: /Account/ExternalLogin
//
// Go to me first!!!
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
return new ExternalLoginResult(provider, Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
}
//
// Go to me second!!!
internal class ExternalLoginResult : ActionResult
{
public ExternalLoginResult(string provider, string returnUrl)
{
Provider = provider;
ReturnUrl = returnUrl;
}
public string Provider { get; private set; }
public string ReturnUrl { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
if(Provider.Equals("Quickbooks Online")){
// oauthLink = Constants.OauthEndPoints.AuthorizeUrl + "?oauth_token=" + RequestToken + "&oauth_callback=" + UriUtility.UrlEncode(oauth_callback_url);
// oauthLink "https://workplace.intuit.com/Connect/Begin?oauth_token=qyprdvh1kZ3dpyobhKfJ54C10xPJPDy0vtwdNCeZmUmbLJdr&oauth_callback=http%3A%2F%2Flocalhost%3A50671%2FOauthResponse" string
string requestToken = ""; // How do I get this?
string oauthCallback = context.HttpContext.Request.Url.Scheme + "/OauthResponse";
//
// Now do I get a request token? How?
string returnUrl = "https://workplace.intuit.com/Connect/Begin" + "?oauth_token=" + requestToken + "&oauth_callback=" + HttpUtility.UrlEncode(oauthCallback);
OAuthWebSecurity.RequestAuthentication(Provider, returnUrl);
}
else{
OAuthWebSecurity.RequestAuthentication(Provider, ReturnUrl);
}
}
}
But from here I get lost. Any help would be appreciated. I've googled alot and I'd guess I've got somewhere north of 40 hours invested so far trying to get this to work.
Thanks,
Will
Intuit Partner Platform currently supports OAuth1.0a. The first leg in the 3-legged OAuth is the procurement of the OAuth request token.
I haven't used MVC4's OAuthWebSecurity; but, based on what I quickly gleaned from the doc/source code, it seems like you need to create a distinct 'QuickBooksClient' (and, open source it?). I'd follow the LinkedInClient class as a recipe. From the IPP docs, the OAuth endpoints are:
Get Request token: https://oauth.intuit.com/oauth/v1/get_request_token
Login Page: https://appcenter.intuit.com/Connect/Begin
Get Access token: https://oauth.intuit.com/oauth/v1/get_access_token