I'm using the a (slight variation) of the InMemoryToken Manager provided by the ApplicationBuilding Block example to authenticate against MySpace using DotNetOpenAuth.
As it seems to me, the tokens returned from MySpace after authentication are not properly urldecoded when ProcessUserAuthorization is being called.
To get the passing through of the tokens to work I'm currently using the following ugly hack to get the TokenManager to find the matching secret. (The hack is not necessary for Twitter authentication)
public string GetTokenSecret(string token)
{
// hack necessary for myspace :(
token = HttpUtility.UrlDecode(token);
string tokenSecret = tokensAndSecrets[token];
....
}
This is my MySpaceConsumer class
public static class MySpaceConsumer
{
public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription
{
RequestTokenEndpoint = new MessageReceivingEndpoint("http://api.myspace.com/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://api.myspace.com/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("http://api.myspace.com/access_token", HttpDeliveryMethods.GetRequest | 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("http://api.myspace.com/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://api.myspace.com/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("http://api.myspace.com/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
static MySpaceConsumer()
{
// Twitter can't handle the Expect 100 Continue HTTP header.
//ServicePointManager.FindServicePoint(GetFavoritesEndpoint.Location).Expect100Continue = false;
}
public static bool IsMySpaceConsumerConfigured
{
get
{
return !string.IsNullOrEmpty(Busker.MVC.Properties.Settings.Default.TwitterConsumerKey) &&
!string.IsNullOrEmpty(Busker.MVC.Properties.Settings.Default.TwitterConsumerSecret);
}
}
private static BuskerTokenManager ShortTermUserSessionTokenManager
{
get
{
var store = HttpContext.Current.Session;
var tokenManager = (BuskerTokenManager)store["MySpaceShortTermUserSessionTokenManager"];
if (tokenManager == null)
{
string consumerKey = Busker.MVC.Properties.Settings.Default.TwitterConsumerKey;
string consumerSecret = Busker.MVC.Properties.Settings.Default.TwitterConsumerSecret;
if (IsMySpaceConsumerConfigured)
{
tokenManager = new BuskerTokenManager(consumerKey, consumerSecret);
store["MySpaceShortTermUserSessionTokenManager"] = tokenManager;
}
else
{
throw new InvalidOperationException("No Twitter OAuth consumer key and secret could be found in web.config AppSettings.");
}
}
return tokenManager;
}
}
private static readonly MessageReceivingEndpoint GetMyProfile = new MessageReceivingEndpoint("http://api.myspace.com/1.0/people/#me/#self?format=xml", HttpDeliveryMethods.GetRequest);
public static XDocument GetProfile(ConsumerBase myspace, string accessToken)
{
IncomingWebResponse response = myspace.PrepareAuthorizedRequestAndSend(GetMyProfile, accessToken);
return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
}
Related
I've been trying to follow a number of tutorials I can find to have an mvc application allow a user to authenticate the app and get the access and refresh tokens back. Unfortunately I can't find any that are clear enough to where I can follow what's going on. I started with google's sample code and then found some others like this one and this one.
When I run my app I'm trying to go to http://localhost:61581/Integration/Google/IndexAsync it hits that method which eventually hits the AppFlowMetadata.GetUserId method and then hits my custom TenixDataStore class' GetAsync method.
The things that are confusing are
First off, am I going to the right url/method? I think I am based on google's code example but not sure.
I thought that the key I would get would be the email address but instead is a GUID. Is that how google identifies a user?
If I'm going to the right url, why does the page just hang and never return. I expected it to open a google authorization page which didn't happen.
Here's my code.
AppFlowMetadata class
using System.Web.Mvc;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Gmail.v1;
using Tenix.Domain.Constants;
namespace MyApp.Areas.Integration.Controllers
{
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = APIConstants.GMailApiKey,
ClientSecret = APIConstants.GmailApiSecret
},
Scopes = new[] {GmailService.Scope.GmailReadonly},
DataStore = new TenixDataStore()
});
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
public override string GetUserId(Controller controller)
{
// In this sample we use the session to store the user identifiers.
// That's not the best practice, because you should have a logic to identify
// a user. You might want to use "OpenID Connect".
// You can read more about the protocol in the following link:
// https://developers.google.com/accounts/docs/OAuth2Login.
var user = controller.Session["UserID"];
if (user == null) return null;
return user.ToString();
}
}
}
GoogleController
using System.Threading;
using System.Threading.Tasks;
using System.Web.Mvc;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Gmail.v1;
using Google.Apis.Services;
namespace MyApp.Areas.Integration.Controllers
{
public class GoogleController : Controller
{
public async Task IndexAsync(CancellationToken cancellationToken)
{
if (Session["UserID"] == null)
{
Response.Redirect("~/Login.aspx", true);
}
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
var service = new GmailService(new BaseClientService.Initializer
{
HttpClientInitializer = result.Credential,
ApplicationName = "Tenix Gmail Integration"
});
}
}
}
}
TenixDataStore class
using System;
using System.Threading.Tasks;
using DataBaseUtilitiesTEN;
using Google.Apis.Json;
using Google.Apis.Util.Store;
using Newtonsoft.Json.Linq;
using Synergy.Extensions;
using Tenix.Domain.Data.Respositories;
using Tenix.Domain.Model.Integration;
using Tenix.Domain.Services;
namespace MyApp.Areas.Integration.Controllers
{
public class TenixDataStore : IDataStore
{
private readonly string conStr = ConnectionStrings.GeneralInfo;
private CredentialService _service;
public TenixDataStore()
{
_service = new CredentialService(new CredentialRepository(conStr));
}
public Task StoreAsync<T>(string key, T value)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key MUST have a value");
var serialized = NewtonsoftJsonSerializer.Instance.Serialize(value);
var jObject = JObject.Parse(serialized);
var access_token = jObject.SelectToken("access_token");
var refresh_token = jObject.SelectToken("refresh_token");
if (access_token == null)
throw new ArgumentException("Missing access token");
if (refresh_token == null)
throw new ArgumentException("Missing refresh token");
_service.SaveUserCredentials(new UserCredential
{
EmailAddress = key,
AccessToken = (string)access_token,
RefreshToken = (string)refresh_token
});
return Task.Delay(0);
}
public Task DeleteAsync<T>(string key)
{
_service.DeleteCredentials(key);
return Task.Delay(0);
}
public Task<T> GetAsync<T>(string userId)
{
var credentials = _service.GetUserCredentials(userId.To<int>());
var completionSource = new TaskCompletionSource<T>();
if (!string.IsNullOrEmpty(credentials.AccessToken))
completionSource.SetResult(NewtonsoftJsonSerializer.Instance.Deserialize<T>(credentials.AccessToken));
return completionSource.Task;
}
public Task ClearAsync()
{
return Task.Delay(0);
}
}
}
AuthCallbackController
using Google.Apis.Auth.OAuth2.Mvc;
namespace MyApp.Areas.Integration.Controllers
{
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override FlowMetadata FlowData
{
get { return new AppFlowMetadata(); }
}
}
}
After spending days trying to figure this out and not making any headway with the google api .net libraries I ended up just going with my own implementation which after reading their documentation was at least something I could fully understand. In case anyone could use the code, here's what I ended up with. Still need to do some refactoring, but at this point it's working.
Just need to make sure the AuthorizeResponse and Authorize routes are registered as authorized redirect uris.
public class GoogleController : Controller
{
private readonly CredentialService _credentialService;
private readonly GoogleEndpoints _endpoints;
public GoogleController()
{
_endpoints = new GoogleEndpoints();
_credentialService = new CredentialService(new CredentialRepository(ConnectionStrings.GeneralInfo));
}
private string AuthorizeUrl
{
get
{
return "/Integration/Google/Authorize";
}
}
private string AuthorizeResponseUrl
{
get
{
return "/Integration/Google/AuthorizeResponse";
}
}
private string SaveResponseUrl
{
get
{
return "/Integration/Google/SaveResponse";
}
}
public void Authorize()
{
if (Session["UserID"] == null || Session["Email"] == null)
{
Response.Redirect("~/Login.aspx", true);
Session["LoginSource"] = AuthorizeUrl;
Response.End();
}
else
{
if (Session["SessionId"] == null || Session["SessionId"].ToString().Trim().Length == 0)
Session["SessionId"] = _credentialService.CreateSessionId(Session["UserID"].To<int>());
var url = _endpoints.AuthorizationEndpoint + "?" +
"client_id=" + APIConstants.GMailApiKey + "&" +
"response_type=code&" +
"scope=openid%20email&" +
"redirect_uri=" + AuthorizeResponseUrl + "&" +
"state=" + Session["SessionId"] + "&" +
"login_hint=" + Session["Email"] + "&" +
"access_type=offline";
Response.Redirect(url);
}
}
public ActionResult AuthorizeResponse()
{
var state = Request.QueryString["state"];
if (state == Session["SessionId"].ToString())
{
var code = Request.QueryString["code"];
var values = new Dictionary<string, object>
{
{"code", code},
{"redirect_uri", AuthorizeResponseUrl},
{"client_id", APIConstants.GMailApiKey},
{"client_secret", APIConstants.GmailApiSecret},
{"grant_type", "authorization_code"},
{"scope", ""}
};
var webmethods = new WebMethods();
var tokenResponse = webmethods.Post(_endpoints.TokenEndpoint, values);
var jobject = JObject.Parse(tokenResponse);
var access_token = jobject.SelectToken("access_token");
var refresh_token = jobject.SelectToken("refresh_token");
if (access_token == null || access_token.ToString().Trim().Length == 0)
{
//notify devs something went wrong
return View(new GoogleAuthResponse(tokenResponse, false));
}
var credentials = _credentialService.GetUserCredentials(Session["SessionId"].ToString());
credentials.AccessToken = access_token.ToString();
credentials.RefreshToken = refresh_token.ToString();
credentials.EmployeeId = Session["UserId"].To<int>();
_credentialService.SaveUserCredentials(credentials);
return View(new GoogleAuthResponse("Integration successful!", true));
}
return View(new GoogleAuthResponse("Missing state information.", false));
}
}
And the helper class to get the google endpoints.
public class GoogleEndpoints
{
public GoogleEndpoints()
{
using (var client = new WebClient())
{
var response = client.DownloadString("https://accounts.google.com/.well-known/openid-configuration");
var jobject = JObject.Parse(response);
AuthorizationEndpoint = jobject.SelectToken("authorization_endpoint").ToString();
TokenEndpoint = jobject.SelectToken("token_endpoint").ToString();
}
}
public string AuthorizationEndpoint { get; private set; }
public string TokenEndpoint { get; private set; }
}
The controller uses another couple of helper classes for parsing the json and posting the form data, but that should be pretty straightforward.
For my WebAPI, I'm using:
public void ConfigureAuth( IAppBuilder app )
{
app.UseIdentityServerBearerTokenAuthentication( new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings[ "ida:Authority" ],
RequiredScopes = new[ ]
{
"XXXXAPI"
}
} );
}
I authenticate okay, but I need to get my roles and other information supplied by the UserInfo endpoint.
Does UseIdentityServerBearerTokenAuthentication do this automatically or is there an event like OpenIdConnectAuthenticationNotifications.AuthroizationCodeReceived that I should be using to set the ClaimsIdentity?
app.UseIdentityServerBearerTokenAuthentication does set the roles and scopes automatically.
For some unkown reason, I only returned data from the GetProfileDataAsync of the UserService only when the caller was UserInfoEndpoint. Once I got rid of this piece of code, all the roles were automatically populated.
You must use OAuth2+JWT and some custom configuration to save user roles and other claims in access token.
Add these values in Web.config
<appSettings>
<add key="as:AudienceId" value="414e1927a3884f68abc79f7283837fd1" />
<add key="as:AudienceSecret" value="qMCdFDQuF23RV1Y-1Gq9L3cF3VmuFwVbam4fMTdAfpo" />
</appSettings>
Write a CustomJwtFormat class
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.DataHandler.Encoder;
using System;
using System.Configuration;
using System.IdentityModel.Tokens;
using Thinktecture.IdentityModel.Tokens;
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string _issuer = string.Empty;
public CustomJwtFormat(string issuer)
{
_issuer = issuer;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
string symmetricKeyAsBase64 = ConfigurationManager.AppSettings["as:AudienceSecret"];
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
}
Create a custom authenticationProvider
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult<object>(null);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
using (AuthRepository _repo = new AuthRepository())
{
User user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("unique_name", context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
}
Configure Your Custom Setting
private static void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new ApplicationOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("http://localhost/")
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private static void ConfigureOAuthTokenConsumption(IAppBuilder app)
{
var issuer = "http://localhost/";
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
byte[] audienceSecret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["as:AudienceSecret"]);
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
public void Configuration(IAppBuilder app(
{
app.UseAutofacMvc();
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
//other codes
}
If you want to get userInfo from access token, do it as follows:
public static string GetUserNameFromOAuth(HttpRequestMessage Request)
{
if (Request.Headers.Contains("Authorization"))
{
var authHeader = Request.Headers.GetValues("Authorization");
var authEncoded = authHeader.FirstOrDefault();
var authList = authEncoded.Split(' ');
var payload = authList[1];
var symmetricKeyAsBase64 = ConfigurationManager.AppSettings["as:AudienceSecret"];
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
string token = JWT.JsonWebToken.Decode(payload, keyByteArray);
var jsonObject = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(token);
var userName = jsonObject.FirstOrDefault(p => p.Key == "unique_name").Value;
return userName;
}
return "";
}
Test in postman:
I am using WebClient, instead of HttpClient, to make some WebAPI calls from MVC controllers. However, I keep getting 401 Unauthorized error. In the following code, I initialize the WebClient with the UseDefaultCredentials = true. I still get the error.
_lazyClient = new Lazy<WebClient> (() => new WebClient {
UseDefaultCredentials = true
});
public static class WebClientWrapper
{
static private string _baseUrl;
static private Lazy<WebClient> _lazyClient;
static private WebClient Client()
{
if (_lazyClient == null)
{
throw new ObjectDisposedException("WebClient has been disposed");
}
//string myUri = _baseUrl;
//CredentialCache cc = new CredentialCache();
//cc.Add(new Uri(myUri), "NTLM", CredentialCache.DefaultNetworkCredentials);
//_lazyClient.Value.Credentials = cc;
return _lazyClient.Value;
}
static public T Execute<T> (string baseUrl, string urlSegment)
{
_baseUrl = baseUrl.Trim('/');
_lazyClient = new Lazy<WebClient> (() => new WebClient {
UseDefaultCredentials = true
});
return JsonConvert.DeserializeObject<T>(Client().DownloadString(_baseUrl + '/' + urlSegment.TrimStart('/')));
}
}
In the last DotNetOpenAuth package, GoogleClient extends OpenIdClient, Someone knows where can i find implementation of google Oauth2 which extends DotNetOpenAuth OAuth2Client?
From OAuth2 and DotNetOpenAuth - implementing Google custom client
public class GoogleOAuth2Client : OAuth2Client
{
#region Constants and Fields
/// <summary>
/// The authorization endpoint.
/// </summary>
private const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth";
/// <summary>
/// The token endpoint.
/// </summary>
private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
/// <summary>
/// The _app id.
/// </summary>
private readonly string _clientId;
/// <summary>
/// The _app secret.
/// </summary>
private readonly string _clientSecret;
#endregion
public const string ProviderAppendix = "__provider__=google";
public GoogleOAuth2Client(string clientId, string clientSecret)
: base("google")
{
if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException("clientId");
if (string.IsNullOrWhiteSpace(clientSecret)) throw new ArgumentNullException("clientSecret");
this._clientId = clientId;
this._clientSecret = clientSecret;
}
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
StringBuilder serviceUrl = new StringBuilder();
serviceUrl.AppendFormat("{0}?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile", AuthorizationEndpoint);
serviceUrl.Append("&state=google");
serviceUrl.AppendFormat("&redirect_uri={0}", returnUrl.ToString().ToLower());
serviceUrl.Append("&response_type=code");
serviceUrl.AppendFormat("&client_id={0}", _clientId);
return new Uri(serviceUrl.ToString());
}
protected override IDictionary<string, string> GetUserData(string accessToken)
{
var client = new RestClient("https://www.googleapis.com");
var request = new RestRequest(String.Format("/oauth2/v1/userinfo?access_token={0}", accessToken), Method.GET);
IDictionary<String, String> extraData = new Dictionary<String, String>();
var response = client.Execute(request);
if (null != response.ErrorException)
{
return null;
}
else
{
try
{
var json = JObject.Parse(response.Content);
string firstName = (string)json["given_name"];
string lastName = (string)json["family_name"];
string emailAddress = (string)json["email"];
string id = (string)json["id"];
extraData = new Dictionary<String, String>
{
{"accesstoken", accessToken},
{"name", String.Format("{0} {1}", firstName, lastName)},
{"firstname", firstName},
{"lastname", lastName},
{"email", emailAddress},
{"id", id}
};
}
catch (Exception ex)
{
Ccl.Log.Logging.Error("Error requesting OAuth user data from Google", ex);
return null;
}
return extraData;
}
}
protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
{
StringBuilder postData = new StringBuilder();
postData.AppendFormat("client_id={0}", this._clientId);
postData.AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(returnUrl.ToString().ToLower()));
postData.AppendFormat("&client_secret={0}", this._clientSecret);
postData.AppendFormat("&grant_type={0}", "authorization_code");
postData.AppendFormat("&code={0}", authorizationCode);
string response = "";
string accessToken = "";
var webRequest = (HttpWebRequest)WebRequest.Create(TokenEndpoint);
webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
try
{
using (Stream s = webRequest.GetRequestStream())
{
using (StreamWriter sw = new StreamWriter(s))
sw.Write(postData.ToString());
}
using (WebResponse webResponse = webRequest.GetResponse())
{
using (var reader = new StreamReader(webResponse.GetResponseStream()))
{
response = reader.ReadToEnd();
}
}
var json = JObject.Parse(response);
accessToken = (string)json["access_token"];
}
catch (Exception ex)
{
Ccl.Log.Logging.Error("Error requesting OAuth access token from Google", ex);
return null;
}
return accessToken;
}
public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl)
{
string code = context.Request.QueryString["code"];
if (string.IsNullOrEmpty(code))
{
return AuthenticationResult.Failed;
}
string accessToken = this.QueryAccessToken(returnPageUrl, code);
if (accessToken == null)
{
return AuthenticationResult.Failed;
}
IDictionary<string, string> userData = this.GetUserData(accessToken);
if (userData == null)
{
return AuthenticationResult.Failed;
}
string id = userData["id"];
string name;
// Some oAuth providers do not return value for the 'username' attribute.
// In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id'
if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name))
{
name = id;
}
// add the access token to the user data dictionary just in case page developers want to use it
userData["accesstoken"] = accessToken;
return new AuthenticationResult(
isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData);
}
}
I am attempting to integrate Google Calendar into my application and I am having some problems with the OAuth authorization passing off a RefreshToken. I receive an AccessToken with no issue, but the RefreshToken property is null. See the line marked "ERROR HERE:" for where I am having the issue
My Asp.Net MVC controller (named OAuthController) looks like the following:
public ActionResult Index()
{
var client = CreateClient();
client.RequestUserAuthorization(new[] { "https://www.googleapis.com/auth/calendar" }, new Uri("http://localhost/FL.Evaluation.Web/OAuth/CallBack"));
return View();
}
public ActionResult CallBack()
{
if (string.IsNullOrEmpty(Request.QueryString["code"])) return null;
var client = CreateClient();
// Now getting a 400 Bad Request here
var state = client.ProcessUserAuthorization();
// ERROR HERE: The RefreshToken is NULL
HttpContext.Session["REFRESH_TOKEN"] = Convert.ToBase64String(Encoding.Unicode.GetBytes(state.RefreshToken));
return JavaScript("Completed!");
}
private static WebServerClient CreateClient()
{
return
new WebServerClient(
new AuthorizationServerDescription()
{
TokenEndpoint = new Uri("https://accounts.google.com/o/oauth2/token"),
AuthorizationEndpoint = new Uri("https://accounts.google.com/o/oauth2/auth"),
ProtocolVersion = ProtocolVersion.V20
}
, _GoogleClientId, _GoogleSecret);
}
I see in Google's API documents, that I need to ensure that the access_type requested is set to offline for a RefreshToken to be sent. How do I set this value in my Authenticator request?
After hours of fiddling with DotNetOpenAuth and the Google APIs published for .Net, I got nowhere fast. I decided to circumvent the libraries and went directly at the Google REST API with native HttpRequest and HttpResponse objects. My sanitized code for my MVC controller follows:
private static string _GoogleClientId = "CLIENT_ID";
private static string _GoogleSecret = "SECRET";
private static string _ReturnUrl = "http://localhost/OAuth/CallBack";
public ActionResult Index()
{
return Redirect(GenerateGoogleOAuthUrl());
}
private string GenerateGoogleOAuthUrl()
{
//NOTE: Key piece here, from Andrew's reply -> access_type=offline forces a refresh token to be issued
string Url = "https://accounts.google.com/o/oauth2/auth?scope={0}&redirect_uri={1}&response_type={2}&client_id={3}&state={4}&access_type=offline&approval_prompt=force";
string scope = UrlEncodeForGoogle("https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.readonly").Replace("%20", "+");
string redirect_uri_encode = UrlEncodeForGoogle(_ReturnUrl);
string response_type = "code";
string state = "";
return string.Format(Url, scope, redirect_uri_encode, response_type, _GoogleClientId, state);
}
private static string UrlEncodeForGoogle(string url)
{
string UnReservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
var result = new StringBuilder();
foreach (char symbol in url)
{
if (UnReservedChars.IndexOf(symbol) != -1)
{
result.Append(symbol);
}
else
{
result.Append('%' + String.Format("{0:X2}", (int)symbol));
}
}
return result.ToString();
}
class GoogleTokenData
{
public string Access_Token { get; set; }
public string Refresh_Token { get; set; }
public string Expires_In { get; set; }
public string Token_Type { get; set; }
}
public ActionResult CallBack(string code, bool? remove)
{
if (remove.HasValue && remove.Value)
{
Session["GoogleAPIToken"] = null;
return HttpNotFound();
}
if (string.IsNullOrEmpty(code)) return Content("Missing code");
string Url = "https://accounts.google.com/o/oauth2/token";
string grant_type = "authorization_code";
string redirect_uri_encode = UrlEncodeForGoogle(_ReturnUrl);
string data = "code={0}&client_id={1}&client_secret={2}&redirect_uri={3}&grant_type={4}";
HttpWebRequest request = HttpWebRequest.Create(Url) as HttpWebRequest;
string result = null;
request.Method = "POST";
request.KeepAlive = true;
request.ContentType = "application/x-www-form-urlencoded";
string param = string.Format(data, code, _GoogleClientId, _GoogleSecret, redirect_uri_encode, grant_type);
var bs = Encoding.UTF8.GetBytes(param);
using (Stream reqStream = request.GetRequestStream())
{
reqStream.Write(bs, 0, bs.Length);
}
using (WebResponse response = request.GetResponse())
{
var sr = new StreamReader(response.GetResponseStream());
result = sr.ReadToEnd();
sr.Close();
}
var jsonSerializer = new JavaScriptSerializer();
var tokenData = jsonSerializer.Deserialize<GoogleTokenData>(result);
Session["GoogleAPIToken"] = tokenData.Access_Token;
return JavaScript("Refresh Token: " + tokenData.Refresh_Token);
}
Big thanks to Kelp for a bit of the code in this snippet.
Adjust GoogleAuthenticationServer.Description to have an authorization endpoint URI that includes ?access_type=offline in the query string.
Simply add
AccessType = "offline",
to GoogleOAuth2AuthenticationOptions() object.