GoogleWebAuthorizationBroker in MVC For Google Drive Access - c#

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

Related

Custom auth "The oauth state was missing or invalid"

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.

VssConnection to VSTS always prompts for credentials

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

Unauthorized GET request for .NET Core RESTful web API

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.

Web API custom authentication filter

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

2-legged OAuth with DotNetOpenAuth and Twitter. Getting a 401 error

I'm trying to get 2-legged oauth with twitter working to be able to retrieve tweets to display on websites. For this i want to use DotNetOpenAuth. To get me started i used the OAuthConsumer example that comes with DotNetOpenAuth.
Below is the codebehind code i have now. Here i use either the 3-legged part or the 2-legged part. The 3-legged part works fine (this is based on Twitter.aspx from the sample). The 2-legged part (based on GoogleApps2Legged.aspx) however gives a 401 error on twitter.RequestNewClientAccount();
private string AccessToken
{
get { return (string)Session["TwitterAccessToken"]; }
set { Session["TwitterAccessToken"] = value; }
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// 3 legged
var twitter = new WebConsumer(TwitterConsumer.ServiceDescription, TwitterConsumer.ShortTermUserSessionTokenManager);
//// Is Twitter calling back with authorization?
var accessTokenResponse = twitter.ProcessUserAuthorization();
if (accessTokenResponse != null)
{
this.AccessToken = accessTokenResponse.AccessToken;
}
else if (this.AccessToken == null)
{
// If we don't yet have access, immediately request it.
twitter.Channel.Send(twitter.PrepareRequestUserAuthorization());
}
var response = TwitterConsumer.GetUserTimeLine(twitter, this.AccessToken);
// 2 legged
var twitter = new WebConsumer(TwitterConsumer.ServiceDescription, TwitterConsumer.ShortTermUserSessionTokenManager);
this.AccessToken = twitter.RequestNewClientAccount();
var response = twitter.PrepareAuthorizedRequestAndSend(TwitterConsumer.GetUserTimelineStatusEndpoint, this.AccessToken);
}
}
TwitterConsumer.cs
//-----------------------------------------------------------------------
// <copyright file="TwitterConsumer.cs" company="Outercurve Foundation">
// Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.ApplicationBlock {
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Net;
using System.Web;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;
/// <summary>
/// A consumer capable of communicating with Twitter.
/// </summary>
public static class TwitterConsumer {
/// <summary>
/// The description of Twitter's OAuth protocol URIs for use with actually reading/writing
/// a user's private Twitter data.
/// </summary>
public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription {
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
/// <summary>
/// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature.
/// </summary>
public static readonly ServiceProviderDescription SignInWithTwitterServiceDescription = new ServiceProviderDescription {
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
/// <summary>
/// The URI to get a user's favorites.
/// </summary>
private static readonly MessageReceivingEndpoint GetFavoritesEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/favorites.xml", HttpDeliveryMethods.GetRequest);
/// <summary>
/// The URI to get the data on the user's home page.
/// </summary>
public static readonly MessageReceivingEndpoint GetFriendTimelineStatusEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/statuses/friends_timeline.xml", HttpDeliveryMethods.GetRequest);
public static readonly MessageReceivingEndpoint UpdateProfileBackgroundImageEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/update_profile_background_image.xml", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
public static readonly MessageReceivingEndpoint UpdateProfileImageEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/update_profile_image.xml", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
public static readonly MessageReceivingEndpoint VerifyCredentialsEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/verify_credentials.xml", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
public static readonly MessageReceivingEndpoint GetUserTimelineStatusEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=concepts2go", HttpDeliveryMethods.GetRequest);
/// <summary>
/// The consumer used for the Sign in to Twitter feature.
/// </summary>
private static WebConsumer signInConsumer;
/// <summary>
/// The lock acquired to initialize the <see cref="signInConsumer"/> field.
/// </summary>
private static object signInConsumerInitLock = new object();
/// <summary>
/// Initializes static members of the <see cref="TwitterConsumer"/> class.
/// </summary>
static TwitterConsumer() {
// Twitter can't handle the Expect 100 Continue HTTP header.
ServicePointManager.FindServicePoint(GetFavoritesEndpoint.Location).Expect100Continue = false;
}
/// <summary>
/// Gets a value indicating whether the Twitter consumer key and secret are set in the web.config file.
/// </summary>
public static bool IsTwitterConsumerConfigured {
get {
return true;
}
}
/// <summary>
/// Gets the consumer to use for the Sign in to Twitter feature.
/// </summary>
/// <value>The twitter sign in.</value>
private static WebConsumer TwitterSignIn {
get {
if (signInConsumer == null) {
lock (signInConsumerInitLock) {
if (signInConsumer == null) {
signInConsumer = new WebConsumer(SignInWithTwitterServiceDescription, ShortTermUserSessionTokenManager);
}
}
}
return signInConsumer;
}
}
public static InMemoryTokenManager ShortTermUserSessionTokenManager {
get {
var store = HttpContext.Current.Session;
var tokenManager = (InMemoryTokenManager)store["TwitterShortTermUserSessionTokenManager"];
if (tokenManager == null) {
string consumerKey = "dgnwPLz40rcvmGKn0SugfA";
string consumerSecret = "IvaV0cA3N146mXTXPH5EaEHX8XbULch1vwBYpeI4Xc";
if (IsTwitterConsumerConfigured) {
tokenManager = new InMemoryTokenManager(consumerKey, consumerSecret);
store["TwitterShortTermUserSessionTokenManager"] = tokenManager;
} else {
throw new InvalidOperationException("No Twitter OAuth consumer key and secret could be found in web.config AppSettings.");
}
}
return tokenManager;
}
}
public static XDocument GetUpdates(ConsumerBase twitter, string accessToken) {
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetFriendTimelineStatusEndpoint, accessToken);
return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static IncomingWebResponse GetUserTimeLine(ConsumerBase twitter, string accessToken)
{
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetUserTimelineStatusEndpoint, accessToken);
return response;
//return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static XDocument GetFavorites(ConsumerBase twitter, string accessToken) {
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetFavoritesEndpoint, accessToken);
return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static XDocument UpdateProfileBackgroundImage(ConsumerBase twitter, string accessToken, string image, bool tile) {
var parts = new[] {
MultipartPostPart.CreateFormFilePart("image", image, "image/" + Path.GetExtension(image).Substring(1).ToLowerInvariant()),
MultipartPostPart.CreateFormPart("tile", tile.ToString().ToLowerInvariant()),
};
HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileBackgroundImageEndpoint, accessToken, parts);
request.ServicePoint.Expect100Continue = false;
IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request);
string responseString = response.GetResponseReader().ReadToEnd();
return XDocument.Parse(responseString);
}
public static XDocument UpdateProfileImage(ConsumerBase twitter, string accessToken, string pathToImage) {
string contentType = "image/" + Path.GetExtension(pathToImage).Substring(1).ToLowerInvariant();
return UpdateProfileImage(twitter, accessToken, File.OpenRead(pathToImage), contentType);
}
public static XDocument UpdateProfileImage(ConsumerBase twitter, string accessToken, Stream image, string contentType) {
var parts = new[] {
MultipartPostPart.CreateFormFilePart("image", "twitterPhoto", contentType, image),
};
HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileImageEndpoint, accessToken, parts);
IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request);
string responseString = response.GetResponseReader().ReadToEnd();
return XDocument.Parse(responseString);
}
public static XDocument VerifyCredentials(ConsumerBase twitter, string accessToken) {
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(VerifyCredentialsEndpoint, accessToken);
return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static string GetUsername(ConsumerBase twitter, string accessToken) {
XDocument xml = VerifyCredentials(twitter, accessToken);
XPathNavigator nav = xml.CreateNavigator();
return nav.SelectSingleNode("/user/screen_name").Value;
}
/// <summary>
/// Prepares a redirect that will send the user to Twitter to sign in.
/// </summary>
/// <param name="forceNewLogin">if set to <c>true</c> the user will be required to re-enter their Twitter credentials even if already logged in to Twitter.</param>
/// <returns>The redirect message.</returns>
/// <remarks>
/// Call <see cref="OutgoingWebResponse.Send"/> or
/// <c>return StartSignInWithTwitter().<see cref="MessagingUtilities.AsActionResult">AsActionResult()</see></c>
/// to actually perform the redirect.
/// </remarks>
public static OutgoingWebResponse StartSignInWithTwitter(bool forceNewLogin) {
var redirectParameters = new Dictionary<string, string>();
if (forceNewLogin) {
redirectParameters["force_login"] = "true";
}
Uri callback = MessagingUtilities.GetRequestUrlFromContext().StripQueryArgumentsWithPrefix("oauth_");
var request = TwitterSignIn.PrepareRequestUserAuthorization(callback, null, redirectParameters);
return TwitterSignIn.Channel.PrepareResponse(request);
}
/// <summary>
/// Checks the incoming web request to see if it carries a Twitter authentication response,
/// and provides the user's Twitter screen name and unique id if available.
/// </summary>
/// <param name="screenName">The user's Twitter screen name.</param>
/// <param name="userId">The user's Twitter unique user ID.</param>
/// <returns>
/// A value indicating whether Twitter authentication was successful;
/// otherwise <c>false</c> to indicate that no Twitter response was present.
/// </returns>
public static bool TryFinishSignInWithTwitter(out string screenName, out int userId) {
screenName = null;
userId = 0;
var response = TwitterSignIn.ProcessUserAuthorization();
if (response == null) {
return false;
}
screenName = response.ExtraData["screen_name"];
userId = int.Parse(response.ExtraData["user_id"]);
// If we were going to make this LOOK like OpenID even though it isn't,
// this seems like a reasonable, secure claimed id to allow the user to assume.
OpenId.Identifier fake_claimed_id = string.Format(CultureInfo.InvariantCulture, "http://twitter.com/{0}#{1}", screenName, userId);
return true;
}
}
}
InMemoryTokenManager.cs
//-----------------------------------------------------------------------
// <copyright file="InMemoryTokenManager.cs" company="Outercurve Foundation">
// Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.ApplicationBlock {
using System;
using System.Collections.Generic;
using System.Diagnostics;
using DotNetOpenAuth.OAuth.ChannelElements;
using DotNetOpenAuth.OAuth.Messages;
using DotNetOpenAuth.OpenId.Extensions.OAuth;
/// <summary>
/// A token manager that only retains tokens in memory.
/// Meant for SHORT TERM USE TOKENS ONLY.
/// </summary>
/// <remarks>
/// A likely application of this class is for "Sign In With Twitter",
/// where the user only signs in without providing any authorization to access
/// Twitter APIs except to authenticate, since that access token is only useful once.
/// </remarks>
public class InMemoryTokenManager : IConsumerTokenManager, IOpenIdOAuthTokenManager {
private Dictionary<string, string> tokensAndSecrets = new Dictionary<string, string>();
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryTokenManager"/> class.
/// </summary>
/// <param name="consumerKey">The consumer key.</param>
/// <param name="consumerSecret">The consumer secret.</param>
public InMemoryTokenManager(string consumerKey, string consumerSecret) {
if (string.IsNullOrEmpty(consumerKey)) {
throw new ArgumentNullException("consumerKey");
}
this.ConsumerKey = consumerKey;
this.ConsumerSecret = consumerSecret;
}
/// <summary>
/// Gets the consumer key.
/// </summary>
/// <value>The consumer key.</value>
public string ConsumerKey { get; private set; }
/// <summary>
/// Gets the consumer secret.
/// </summary>
/// <value>The consumer secret.</value>
public string ConsumerSecret { get; private set; }
#region ITokenManager Members
/// <summary>
/// Gets the Token Secret given a request or access token.
/// </summary>
/// <param name="token">The request or access token.</param>
/// <returns>
/// The secret associated with the given token.
/// </returns>
/// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception>
public string GetTokenSecret(string token) {
return this.tokensAndSecrets[token];
}
/// <summary>
/// Stores a newly generated unauthorized request token, secret, and optional
/// application-specific parameters for later recall.
/// </summary>
/// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param>
/// <param name="response">The response message that includes the unauthorized request token.</param>
/// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception>
/// <remarks>
/// Request tokens stored by this method SHOULD NOT associate any user account with this token.
/// It usually opens up security holes in your application to do so. Instead, you associate a user
/// account with access tokens (not request tokens) in the <see cref="ExpireRequestTokenAndStoreNewAccessToken"/>
/// method.
/// </remarks>
public void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) {
this.tokensAndSecrets[response.Token] = response.TokenSecret;
}
/// <summary>
/// Deletes a request token and its associated secret and stores a new access token and secret.
/// </summary>
/// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param>
/// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param>
/// <param name="accessToken">The new access token that is being issued to the Consumer.</param>
/// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param>
/// <remarks>
/// <para>
/// Any scope of granted privileges associated with the request token from the
/// original call to <see cref="StoreNewRequestToken"/> should be carried over
/// to the new Access Token.
/// </para>
/// <para>
/// To associate a user account with the new access token,
/// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be
/// useful in an ASP.NET web application within the implementation of this method.
/// Alternatively you may store the access token here without associating with a user account,
/// and wait until <see cref="WebConsumer.ProcessUserAuthorization()"/> or
/// <see cref="DesktopConsumer.ProcessUserAuthorization(string, string)"/> return the access
/// token to associate the access token with a user account at that point.
/// </para>
/// </remarks>
public void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) {
this.tokensAndSecrets.Remove(requestToken);
this.tokensAndSecrets[accessToken] = accessTokenSecret;
}
/// <summary>
/// Classifies a token as a request token or an access token.
/// </summary>
/// <param name="token">The token to classify.</param>
/// <returns>Request or Access token, or invalid if the token is not recognized.</returns>
public TokenType GetTokenType(string token) {
throw new NotImplementedException();
}
#endregion
#region IOpenIdOAuthTokenManager Members
/// <summary>
/// Stores a new request token obtained over an OpenID request.
/// </summary>
/// <param name="consumerKey">The consumer key.</param>
/// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param>
/// <remarks>
/// <para>The token secret is the empty string.</para>
/// <para>Tokens stored by this method should be short-lived to mitigate
/// possible security threats. Their lifetime should be sufficient for the
/// relying party to receive the positive authentication assertion and immediately
/// send a follow-up request for the access token.</para>
/// </remarks>
public void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization) {
this.tokensAndSecrets[authorization.RequestToken] = string.Empty;
}
#endregion
}
}
Twitter doesn't really support what I call 2-legged OAuth. It only does "0-legged OAuth". Since the ConsumerBase.RequestNewClientAccount method that you're calling is for 2-legged OAuth, it's failing.
For 0-legged OAuth, you need to create an InMemoryTokenManager that is prefilled with your consumer key, secret and access token and secret. Then pass that token manager into your ConsumerBase-derived type (WebConsumer or DesktopConsumer) and begin making authorized calls.
Or much more simply, you can download the DotNetOpenAuth v4.3 preview that includes an DelegatingHandler where you can completely skip the above steps and just inject your key, token and secrets into one simple method and start making calls with HttpClient that are automatically OAuth 1 signed.

Categories

Resources