6

I'm trying to make a login for a UWP app that I'm developing for a client that has a @<theircompay>.com email that uses G Suite. It doesn't have to access any user data, they just want it as an authentication so that only people that have a company email can access the app.

It would be great if they could login from within the app without having to use a web browser, and even better if it could remember them so they wouldn't have to login every single time.

I've been looking at OAuth 2.0 and several other solutions google has but can't really understand which one to use and much less how.

I looked into this answer but it doesn't seem like a good idea to ship your certificate file with your app.

So basically if this can be done, what (if any) certificates or credentials do I need to get from Google, and how would I handle them and the login through my C# code?

Edit

The app is 100% client side, no server backend

Community
  • 1
  • 1
Andres de Lago
  • 111
  • 3
  • 9
  • 1
    As noted by Romasz, we don't support UWP in the Google API client libraries at the moment. When we do, I strongly expect that authentication via a web browser will be the preferred option, but you should be able to cache those credentials. – Jon Skeet May 13 '17 at 09:45

3 Answers3

3

Taking a look at Google's GitHub it seems that .Net API is still not ready for UWP (however if you traverse the issues you will find that they are working on it, so it's probably a matter of time when official version is ready and this answer would be obsolete).

As I think getting simple accessToken (optionaly refresing it) to basic profile info should be sufficient for this case. Basing on available samples from Google I've build a small project (source at GitHub), that can help you.

So first of all you have to define your app at Google's developer console and obtain ClientID and ClientSecret. Once you have this you can get to coding. To obtain accessToken I will use a WebAuthenticationBroker:

string authString = "https://accounts.google.com/o/oauth2/auth?client_id=" + ClientID;
authString += "&scope=profile";
authString += $"&redirect_uri={RedirectURI}";
authString += $"&state={state}";
authString += $"&code_challenge={code_challenge}";
authString += $"&code_challenge_method={code_challenge_method}";
authString += "&response_type=code";

var receivedData = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.UseTitle, new Uri(authString), new Uri(ApprovalEndpoint));

switch (receivedData.ResponseStatus)
{
    case WebAuthenticationStatus.Success:
        await GetAccessToken(receivedData.ResponseData.Substring(receivedData.ResponseData.IndexOf(' ') + 1), state, code_verifier);
        return true;
    case WebAuthenticationStatus.ErrorHttp:
        Debug.WriteLine($"HTTP error: {receivedData.ResponseErrorDetail}");
        return false;

    case WebAuthenticationStatus.UserCancel:
    default:
        return false;
}

If everything goes all right and user puts correct credentials, you will have to ask Google for tokens (I assume that you only want the user to put credentials once). For this purpose you have the method GetAccessToken:

// Parses URI params into a dictionary - ref: http://stackoverflow.com/a/11957114/72176 
Dictionary<string, string> queryStringParams = data.Split('&').ToDictionary(c => c.Split('=')[0], c => Uri.UnescapeDataString(c.Split('=')[1]));

StringContent content = new StringContent($"code={queryStringParams["code"]}&client_secret={ClientSecret}&redirect_uri={Uri.EscapeDataString(RedirectURI)}&client_id={ClientID}&code_verifier={codeVerifier}&grant_type=authorization_code",
                                          Encoding.UTF8, "application/x-www-form-urlencoded");

HttpResponseMessage response = await httpClient.PostAsync(TokenEndpoint, content);
string responseString = await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
{
    Debug.WriteLine("Authorization code exchange failed.");
    return;
}

JsonObject tokens = JsonObject.Parse(responseString);
accessToken = tokens.GetNamedString("access_token");

foreach (var item in vault.RetrieveAll().Where((x) => x.Resource == TokenTypes.AccessToken.ToString() || x.Resource == TokenTypes.RefreshToken.ToString())) vault.Remove(item);

vault.Add(new PasswordCredential(TokenTypes.AccessToken.ToString(), "MyApp", accessToken));
vault.Add(new PasswordCredential(TokenTypes.RefreshToken.ToString(), "MyApp", tokens.GetNamedString("refresh_token")));
TokenLastAccess = DateTimeOffset.UtcNow;

Once you have the tokens (I'm saving them in PasswordVault for safety), you can later then use them to authenticate without asking the user for his credentials. Note that accessToken has limited lifetime, therefore you use refreshToken to obtain a new one:

if (DateTimeOffset.UtcNow < TokenLastAccess.AddSeconds(3600))
{
    // is authorized - no need to Sign In
    return true;
}
else
{
    string token = GetTokenFromVault(TokenTypes.RefreshToken);
    if (!string.IsNullOrWhiteSpace(token))
    {
        StringContent content = new StringContent($"client_secret={ClientSecret}&refresh_token={token}&client_id={ClientID}&grant_type=refresh_token",
                                                  Encoding.UTF8, "application/x-www-form-urlencoded");

        HttpResponseMessage response = await httpClient.PostAsync(TokenEndpoint, content);
        string responseString = await response.Content.ReadAsStringAsync();

        if (response.IsSuccessStatusCode)
        {
            JsonObject tokens = JsonObject.Parse(responseString);

            accessToken = tokens.GetNamedString("access_token");

            foreach (var item in vault.RetrieveAll().Where((x) => x.Resource == TokenTypes.AccessToken.ToString())) vault.Remove(item);

            vault.Add(new PasswordCredential(TokenTypes.AccessToken.ToString(), "MyApp", accessToken));
            TokenLastAccess = DateTimeOffset.UtcNow;
            return true;
        }
    }
}

The code above is only a sample (with some shortcuts) and as mentioned above - a working version with some more error handling you will find at my GitHub. Please also note, that I haven't spend much time on this and it will surely need some more work to handle all the cases and possible problems. Though hopefully will help you to start.

Romasz
  • 29,412
  • 12
  • 76
  • 146
  • This is great, exactly what I needed. Thank you for the full code on GitHub – Andres de Lago May 13 '17 at 18:45
  • should we really hardcode client secret in the source code? – Emil Jun 09 '18 at 12:01
  • @batmaci Somehow you need the client secret, [it's role seems to be reduced](https://stackoverflow.com/a/12163484/2681948), nevertheless it's still required. – Romasz Jun 09 '18 at 19:14
  • i tried your code but somehow it doest work. it keeps returning ""We can't connect to the service you need right now .Check your network connection and try this again later."" error. do you have any idea? i double checked my clientid and secret seems to be correct – Emil Jun 10 '18 at 01:00
  • @batmaci Not sure. Check the developer console at google. – Romasz Jun 10 '18 at 05:27
  • I think that it has something to do with redirecturl. is it really the correct usage? urn:ietf:wg:oauth:2.0:oob. dont we need to register this url in the google console? but it doesnt accept as it requires http or https. – Emil Jun 10 '18 at 11:55
1

Answer from Roamsz is great but didnt work for me because I found some conflicts or at least with the latest build 17134 as target, it doesn't work. Here are the problem, in his Github sample, he is using returnurl as urn:ietf:wg:oauth:2.0:oob . this is the type of url, you can't use with web application type when you create new "Create OAuth client ID" in the google or firebase console. you must use "Ios" as shown below. because web application requires http or https urls as return url.

from google doc

enter image description here

enter image description here

According to his sample he is using Client secret to obtain access token, this is not possible if you create Ios as type. because Android and Ios arent using client secret. It is perfectly described over here

client_secret The client secret obtained from the API Console. This value is not needed for clients registered as Android, iOS, or Chrome applications.

So you must use type as Ios, No Client Secret needed and return url is urn:ietf:wg:oauth:2.0:oob or urn:ietf:wg:oauth:2.0:oob:auto difference is that auto closes browser and returns back to the app. other one, code needs to be copied manually. I prefer to use urn:ietf:wg:oauth:2.0:oob:auto

Regarding code: please follow his github code. Just remove the Client Secret from the Access Token Request.

EDIT: it looks like I was right that even offical sample is not working after UWP version 15063, somebody created an issue on their github

https://github.com/Microsoft/Windows-universal-samples/issues/642

Emil
  • 5,801
  • 6
  • 51
  • 98
0

I'm using pretty straightforward code with Google.Apis.Oauth2.v2 Nuget package. Note, that I'm using v.1.25.0.859 of that package. I tried to update to the lastest version (1.37.0.1404), but this surprisingly doesn't work with UWP. At the same time v. 1.25.0.859 works just fine.

So, unless there's a better option, I would recommend to use a bit old, but working version of Nuget package.

This is my code:

            credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
                new Uri("ms-appx:///Assets/User/Auth/google_client_secrets.json"),
                new[] { "profile", "email" },
                "me",
                CancellationToken.None);

            await GoogleWebAuthorizationBroker.ReauthorizeAsync(credential, CancellationToken.None);

Then you can retrieve access token from: credential.Token.AccessToken.

Mike Keskinov
  • 10,952
  • 6
  • 57
  • 81