In ProviderSignInController we have a method oauth2ErrorCallback.
When the user cancel authorization in facebook this method redirect to:
/signin?error=access_denied&error_description=Permissions+error
I need customize this url, how can do it?
I need redirect to my page, customized.
For now, I overrride the ProvidersignInCrontroller, but my feeling say to me that this is not the definitive solution
class MyProviderSignInController extends ProviderSignInController {
private final Logger log = LoggerFactory.getLogger(MyProviderSignInController.class);
private String urlToOauth2ErrorCallback;
public MyProviderSignInController(ConnectionFactoryLocator connectionFactoryLocator,
UsersConnectionRepository usersConnectionRepository, SignInAdapter signInAdapter) {
super(connectionFactoryLocator, usersConnectionRepository, signInAdapter);
}
public void defineUrlToOauth2ErrorCallback(String urlToOauth2ErrorCallback) {
this.urlToOauth2ErrorCallback = urlToOauth2ErrorCallback;
}
#RequestMapping(value = "/{providerId}", method = RequestMethod.GET, params = "error")
public RedirectView oauth2ErrorCallback(#PathVariable String providerId, #RequestParam("error") String error,
#RequestParam(value = "error_description", required = false) String errorDescription,
#RequestParam(value = "error_uri", required = false) String errorUri, NativeWebRequest request) {
log.warn("Error during authorization: " + error);
if (urlToOauth2ErrorCallback != null) {
return new RedirectView(urlToOauth2ErrorCallback, false);
}
return super.oauth2ErrorCallback(providerId, error, errorDescription, errorUri, request);
}
}
Related
i have a www.api.com and a www.client.com
all registration will be done at api.com and login will be done at api.com. client.com will only be able to see the UI of the login form.
after user login and api.com return a token to user. How to i use the token to access the rest of the webapi in the api.com? i want to access the GetExployeeByID method. after use login. i stored the token in the sessionStorage.setItem('token', data.access_token)
api method
[RoutePrefix("api/Customer")]
public class CustomerController : ApiController
{
List<customer> list = new List<customer>() { new customer {id=1 ,customerName="Marry",age=13},
new customer { id = 2, customerName = "John", age = 24 } };
[Route("GetExployeeByID/{id:long}")]
[HttpGet]
[Authorize]
public customer GetExployeeByID(long id)
{
return list.FirstOrDefault(x=>x.id==id);
}
}
update 1
this is my ajax post to call the api after login
function lgetemp() {
$.ajax({
url: 'http://www.azapi.com:81/api/customer/GetExployeeByID/1',
datatype:"json",
type: 'get',
headers: {
"access_token":sessionStorage.getItem("token")
},
crossDomain: true,
success: function (data) {
debugger
alert(data.customerName)
},
error: function (err) {
debugger
alert('error')
}
})
}
You should pass the token in the header of the request from the client to the api
Authorization Basic yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY=
The from your API you can query the headers and pull out the token.
string authorizationHeader = HttpContext.Current.Request.Headers["Authorization"];
string toke = authorizationHeader.Replace("Bearer ", String.Empty);
What I've done on my latest project is have a class AuthToken that does a lot of this for me
public class AuthToken : IAuthToken
{
private string _raw;
private IDictionary<string, string> _deserialized;
public string Raw
{
get
{
if (String.IsNullOrWhiteSpace(_raw))
{
string authorizationHeader = HttpContext.Current.Request.Headers["Authorization"];
_raw = authorizationHeader.Replace("Bearer ", String.Empty);
}
return _raw;
}
}
public IDictionary<string, string> Deserialized
{
get
{
if (_deserialized == null)
{
string[] tokenSplit = Raw.Split('.');
string payload = tokenSplit[1];
byte[] payloadBytes = Convert.FromBase64String(payload);
string payloadDecoded = Encoding.UTF8.GetString(payloadBytes);
_deserialized = JsonConvert.DeserializeObject<IDictionary<string, string>>(payloadDecoded);
}
return _deserialized;
}
}
}
Then I inject that into a UserContext class that I can inject into my controllers etc. The user context can then pull out claims from the token as needed. (assuming its a JWT)
public class UserContext : IUserContext
{
private IList<Claim> _claims;
private string _identifier;
private string _email;
private string _clientId;
public IAuthToken Token { get; }
public IList<Claim> Claims
{
get
{
return _claims ?? (_claims = Token.Deserialized.Select(self => new Claim(self.Key, self.Value)).ToList());
}
}
public string Identifier => _identifier ?? (_identifier = Token.Deserialized.ContainsKey("sub") ? Token.Deserialized["sub"] : null);
public string Email => _email ?? (_email = Token.Deserialized.ContainsKey(ClaimTypes.Email) ? Token.Deserialized[ClaimTypes.Email] : null);
public UserContext(IAuthToken authToken)
{
Token = authToken;
}
}
You need to pass the token to the request header and make the call to the API url. Below function can be called by passing the URL and token which you have.
static string CallApi(string url, string token)
{
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient())
{
if (!string.IsNullOrWhiteSpace(token))
{
var t = JsonConvert.DeserializeObject<Token>(token);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + t.access_token);
}
var response = client.GetAsync(url).Result;
return response.Content.ReadAsStringAsync().Result;
}
}
Refer- Token based authentication in Web API for a detailed explanation.
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.
I've done authentication via VK, Instagram, Facebook in my site by template below.
However google requires "Redirect URL".
My redirect URL is like this:
http://localhost:4588/main/AuthenticationCallback?__provider__=google%2B&__sid__=6f3cc5957e4742758719f9b7decc2c09
Parameter "sid" is random every time. So I can't give google precise URL. I tried to input http://localhost:4588/main/AuthenticationCallback as I did for Instagram and it worked for Instagram but Google keeps showing me "400 Error: redirect_uri_mismatch"
I've also tried to pass http://localhost:4588/main/AuthenticationCallback as URL parameter in authorization url to google below. But in this case method "IAuthenticationClient.RequestAuthentication" is not called at all.
Can you advise me what should I input as "Redirect URL" for my Google app?
Template class working with OAuth2:
public class GoogleAuthenticationClient : IAuthenticationClient
{
public string appId;
public string appSecret;
private string redirectUri;
public GoogleAuthenticationClient(string appId, string appSecret)
{
this.appId = appId;
this.appSecret = appSecret;
}
string IAuthenticationClient.ProviderName
{
get { return "google+"; }
}
void IAuthenticationClient.RequestAuthentication(HttpContextBase context, Uri returnUrl)
{
var APP_ID = this.appId;
this.redirectUri = context.Server.UrlEncode(returnUrl.ToString());
var address = String.Format(
"https://accounts.google.com/o/oauth2/auth?client_id={0}&redirect_uri={1}&response_type=code&scope={2}",
APP_ID, this.redirectUri, "https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email"
);
HttpContext.Current.Response.Redirect(address, false);
}
class AccessToken
{
public string access_token = null;
public string user_id = null;
}
class UserData
{
public string uid = null;
public string first_name = null;
public string last_name = null;
public string photo_50 = null;
}
class UsersData
{
public UserData[] response = null;
}
AuthenticationResult IAuthenticationClient.VerifyAuthentication(HttpContextBase context)
{
try
{
string code = context.Request["code"];
var address = String.Format(
"https://accounts.google.com/o/oauth2/token?client_id={0}&client_secret={1}&code={2}&redirect_uri={3}",
this.appId, this.appSecret, code, this.redirectUri);
var response = GoogleAuthenticationClient.Load(address);
var accessToken = GoogleAuthenticationClient.DeserializeJson<AccessToken>(response);
address = String.Format(
"https://www.googleapis.com/plus/v1/people/{0}?access_token=1/fFBGRNJru1FQd44AzqT3Zg",
accessToken.user_id);
response = GoogleAuthenticationClient.Load(address);
var usersData = GoogleAuthenticationClient.DeserializeJson<UsersData>(response);
var userData = usersData.response.First();
return new AuthenticationResult(
true, (this as IAuthenticationClient).ProviderName, accessToken.user_id,
userData.first_name + " " + userData.last_name,
new Dictionary<string, string>());
}
catch (Exception ex)
{
return new AuthenticationResult(ex);
}
}
public static string Load(string address)
{
var request = WebRequest.Create(address) as HttpWebRequest;
using (var response = request.GetResponse() as HttpWebResponse)
{
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
return reader.ReadToEnd();
}
}
}
public static T DeserializeJson<T>(string input)
{
var serializer = new JavaScriptSerializer();
return serializer.Deserialize<T>(input);
}
}
Code in my Controller:
public void ExternalLogin(string provider)
{
OAuthWebSecurity.RegisterClient(
client: new GoogleAuthenticationClient(
"APP_ID", "APP_CODE"),
displayName: "google+", // надпись на кнопке
extraData: null);
ExternalLoginCallback(provider);
}
public void ExternalLoginCallback(string provider)
{
OAuthWebSecurity.RequestAuthentication(provider, Url.Action("AuthenticationCallback"));
}
public ActionResult AuthenticationCallback()
{
var result = OAuthWebSecurity.VerifyAuthentication();
if (result.IsSuccessful == false)
{
return null;
}
else
{
var provider = result.Provider;
var uniqueUserID = result.ProviderUserId;
return RedirectToAction("Main", "Main");
}
}
You can authorise a redirect URI as explained below, but you can't add any parameters to the redirect uri, please see this answer on how the parameters can be passed to Google google oauth2 redirect_uri with several parameters
The authorised redirect URI needs to be set when you created your client ("APP_ID", "APP_CODE") on the Google Cloud Console. Simply navigate to the API console for your project and edit the Web client to set the correct redirect URI you would like to use.
I'm experiencing a situation that I have find nowhere in other articles. I'm designing a RESTful server to be consumed by an mobile app. In this case, username and password are part of header in app call, there is no logon screen.
The following code does the job of validating user info and the controllers has security controlled.
My question is: how can I populate iPrincipal in the ApiController controllers?
I have created a filter addressed at WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Filtro de login
config.Filters.Add(new tbAuthorize());
The code for tbAuthorize is:
public class tbAuthorize : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
string username;
string password;
if (GetUserNameAndPassword(actionContext, out username, out password))
{
if (!isUserAuthorized(username, password))
return false;
else
{
//Users = username;
return true;
}
}
else
return false;
}
private bool GetUserNameAndPassword(HttpActionContext actionContext, out string username, out string password)
{
username = "";
password = "";
if (actionContext.Request.Headers.Authorization == null) return false;
// Convert 64 code to separated string[]
string[] s = ParseAuthHeader(actionContext.Request.Headers.Authorization.ToString());
if (s == null)
return false;
username = s[0];
password = s[1];
return true;
}
private string[] ParseAuthHeader(string authHeader)
{
// Check this is a Basic Auth header
if (authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic")) return null;
// Pull out the Credentials with are seperated by ':' and Base64 encoded
string base64Credentials = authHeader.Substring(6);
string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' });
if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[0])) return null;
// Okay this is the credentials
return credentials;
}
private bool isUserAuthorized(string username, string password)
{
// Valid the user at database
var userId = new UsersController().Login(username, password);
// Membership.GetUser() is null
//Users = Membership.GetUser().Email;
return userId != 0;
}
}
The issue is that I have no access to a cookie in Response and I did not find to way to populate iPrincipal.
I need to has data in this.User.Identity.Name, as this:
[tbAuthorize]
public class UsersController : ApiController
{
public void test()
{
string x = this.User.Identity.Name;
}
Thanks for any help,
Marco Castro
Authentication and Authorization are two differents things. Before authorizing a user you have to authenticate their.
With WebApi you have the concept of pipeline with Delegatinghandler. Message goes from one handler to the next until one send the response. I recommend you to create a DelegatingHandler to authentificate users. Then you can use AuthorizeAttribute to prevent unauthenticated user to access your API.
Here's an example to authenticate user with HTTP Basic
public abstract class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic Realm=\"{0}\"";
protected BasicAuthMessageHandler()
{
}
protected BasicAuthMessageHandler(HttpConfiguration httpConfiguration)
{
InnerHandler = new HttpControllerDispatcher(httpConfiguration);
}
protected virtual string GetRealm(HttpRequestMessage message)
{
return message.RequestUri.Host;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Process request
AuthenticationHeaderValue authValue = request.Headers.Authorization;
if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter) &&
string.Equals(authValue.Scheme, "basic", StringComparison.OrdinalIgnoreCase))
{
// Try to authenticate user
IPrincipal principal = ValidateHeader(authValue.Parameter);
if (principal != null)
{
request.GetRequestContext().Principal = principal;
}
}
return base.SendAsync(request, cancellationToken) // Send message to the InnerHandler
.ContinueWith(task =>
{
// Process response
var response = task.Result;
if (response.StatusCode == HttpStatusCode.Unauthorized &&
!response.Headers.Contains(BasicAuthResponseHeader))
{
response.Headers.Add(BasicAuthResponseHeader,
string.Format(BasicAuthResponseHeaderValue, GetRealm(request)));
}
return response;
}, cancellationToken);
}
private IPrincipal ValidateHeader(string authHeader)
{
// Decode the authentication header & split it
var fromBase64String = Convert.FromBase64String(authHeader);
var lp = Encoding.Default.GetString(fromBase64String);
if (string.IsNullOrWhiteSpace(lp))
return null;
string login;
string password;
int pos = lp.IndexOf(':');
if (pos < 0)
{
login = lp;
password = string.Empty;
}
else
{
login = lp.Substring(0, pos).Trim();
password = lp.Substring(pos + 1).Trim();
}
return ValidateUser(login, password);
}
protected abstract IPrincipal ValidateUser(string userName, string password);
}
Write you own user validation logic. For example:
public class SampleBasicAuthMessageHandler : BasicAuthMessageHandler
{
protected override IPrincipal ValidateUser(string userName, string password)
{
if (string.Equals(userName, "Meziantou", StringComparison.OrdinalIgnoreCase) && password == "123456")
return new GenericPrincipal(new GenericIdentity(userName, "Basic"), new string[0]);
return null;
}
}
Finally you have to register the Handler
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.MessageHandlers.Add(new SampleBasicAuthMessageHandler());
You'll find a complete example on Github: https://github.com/meziantou/Samples/tree/master/Web%20Api%20-%20Basic%20Authentication
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.