Google Cloud Console
- Create project if it doesn't exist yet
- Select your project
- Add Google Drive API
- Set up OAuth consent screen
- Make sure to add your test user
- Create Credentials
- Windows OAuth client ID
- choose Universal Windows Platform (UWP)
- set Store ID to test. Will need to be different for a real app
- Android OAuth client ID
- choose Android
- set package name to the same as project app identifier
- Set SHA-1 certificate fingerprint to
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
. For a real app you will need to create to your own. For Windows, you can install Java that will have keytool in a bin folder that you can use. - Enabled custom URI scheme
.NET MAUI Project
Start from a new project
Install the following NuGet packages
- Google.Apis.Auth
- Google.Apis.Drive.v3
- Google.Apis.Oauth2.v2
Set up Android
Add the file WebAuthenticatorCallbackActivity.cs to Platform/Android folder with the following content:
using Android.App;
using Android.Content;
using Android.Content.PM;
namespace OAuthSample.Platforms.Android;
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
[IntentFilter(new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = CALLBACK_SCHEME)]
public class WebAuthenticationCallbackActivity : Microsoft.Maui.Authentication.WebAuthenticatorCallbackActivity
{
const string CALLBACK_SCHEME = "com.companyname.oauthsample";
}
Set up GoogleDrive Service
Add the file GoogleDriveService.cs to Services folder with the following content (set your UWP and android client id at the top):
using Google.Apis.Auth.OAuth2;
using Google.Apis.Drive.v3;
using Google.Apis.Oauth2.v2;
using Google.Apis.Services;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
namespace OAuthSample.Services;
public class GoogleDriveService
{
readonly string _windowsClientId = "__UWP_CLIENT_ID_HERE__"; // UWP client
readonly string _androidClientId = "__ANDROID_CLIENT_ID_HERE__"; // Android client
Oauth2Service? _oauth2Service;
DriveService? _driveService;
GoogleCredential? _credential;
string? _email;
public bool IsSignedIn => _credential != null;
public string? Email => _email;
public async Task Init()
{
var hasRefreshToken = await SecureStorage.GetAsync("refresh_token") is not null;
if (!IsSignedIn && hasRefreshToken)
{
await SignIn();
}
}
public async Task SignIn()
{
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var expiresIn = Preferences.Get("access_token_epires_in", 0L);
var isExpired = now - 10 > expiresIn; // 10 second buffer
var hasRefreshToken = await SecureStorage.GetAsync("refresh_token") is not null;
if (isExpired && hasRefreshToken)
{
Debug.WriteLine("Using refresh token");
await RefreshToken();
}
else if (isExpired) // No refresh token
{
Debug.WriteLine("Starting auth code flow");
if (DeviceInfo.Current.Platform == DevicePlatform.WinUI)
{
await DoAuthCodeFlowWindows();
}
else if (DeviceInfo.Current.Platform == DevicePlatform.Android)
{
await DoAuthCodeFlowAndroid();
}
else
{
throw new NotImplementedException($"Auth flow for platform {DeviceInfo.Current.Platform} not implemented.");
}
}
var accesToken = await SecureStorage.GetAsync("access_token");
_credential = GoogleCredential.FromAccessToken(accesToken);
_oauth2Service = new Oauth2Service(new BaseClientService.Initializer
{
HttpClientInitializer = _credential,
ApplicationName = "yeetmedia3"
});
_driveService = new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = _credential,
ApplicationName = "yeetmedia3"
});
var userInfo = await _oauth2Service.Userinfo.Get().ExecuteAsync();
_email = userInfo.Email;
}
public async Task<string> ListFiles()
{
var request = _driveService!.Files.List();
var fileList = await request.ExecuteAsync();
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("Files:");
stringBuilder.AppendLine();
if (fileList.Files != null && fileList.Files.Count > 0)
{
foreach (var file in fileList.Files)
{
stringBuilder.AppendLine($"Files: {file.Name} ({file.Id}");
}
}
else
{
stringBuilder.AppendLine("No files found.");
}
return stringBuilder.ToString();
}
public async Task SignOut()
{
await RevokeTokens();
}
private async Task DoAuthCodeFlowWindows()
{
var authUrl = "https://accounts.google.com/o/oauth2/v2/auth";
var clientId = _windowsClientId;
var localPort = 12345;
var redirectUri = $"http://localhost:{localPort}";
var codeVerifier = GenerateCodeVerifier();
var codeChallenge = GenerateCodeChallenge(codeVerifier);
var parameters = GenerateAuthParameters(redirectUri, clientId, codeChallenge);
var queryString = string.Join("&", parameters.Select(param => $"{param.Key}={param.Value}"));
var fullAuthUrl = $"{authUrl}?{queryString}";
await Launcher.OpenAsync(fullAuthUrl);
var authorizationCode = await StartLocalHttpServerAsync(localPort);
await GetInitialToken(authorizationCode, redirectUri, clientId, codeVerifier);
}
private async Task DoAuthCodeFlowAndroid()
{
var authUrl = "https://accounts.google.com/o/oauth2/v2/auth";
var clientId = _androidClientId;
var redirectUri = "com.companyname.yeetmedia3://"; // requires a period: https://developers.google.com/identity/protocols/oauth2/native-app#android
var codeVerifier = GenerateCodeVerifier();
var codeChallenge = GenerateCodeChallenge(codeVerifier);
var parameters = GenerateAuthParameters(redirectUri, clientId, codeChallenge);
var queryString = string.Join("&", parameters.Select(param => $"{param.Key}={param.Value}"));
var fullAuthUrl = $"{authUrl}?{queryString}";
#pragma warning disable CA1416
var authCodeResponse = await WebAuthenticator.AuthenticateAsync(new Uri(fullAuthUrl), new Uri("com.companyname.yeetmedia3://"));
#pragma warning restore CA1416
var authorizationCode = authCodeResponse.Properties["code"];
await GetInitialToken(authorizationCode, redirectUri, clientId, codeVerifier);
}
private static Dictionary<string, string> GenerateAuthParameters(string redirectUri, string clientId, string codeChallenge)
{
return new Dictionary<string, string>
{
//{ "scope", "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.appdata" },
{ "scope", string.Join(' ', [Oauth2Service.Scope.UserinfoProfile, Oauth2Service.Scope.UserinfoEmail, DriveService.Scope.Drive, DriveService.Scope.DriveFile, DriveService.Scope.DriveAppdata]) },
{ "access_type", "offline" },
{ "include_granted_scopes", "true" },
{ "response_type", "code" },
//{ "state", "state_parameter_passthrough_value" },
{ "redirect_uri", redirectUri },
{ "client_id", clientId },
{ "code_challenge_method", "S256" },
{ "code_challenge", codeChallenge },
//{ "prompt", "consent" }
};
}
private static async Task GetInitialToken(string authorizationCode, string redirectUri, string clientId, string codeVerifier)
{
var tokenEndpoint = "https://oauth2.googleapis.com/token";
var client = new HttpClient();
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
{
Content = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", authorizationCode),
new KeyValuePair<string, string>("redirect_uri", redirectUri),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("code_verifier", codeVerifier)
])
};
var response = await client.SendAsync(tokenRequest);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception($"Error requesting token: {responseBody}");
Debug.WriteLine($"Access token: {responseBody}");
var jsonToken = JsonObject.Parse(responseBody);
var accessToken = jsonToken!["access_token"]!.ToString();
var refreshToken = jsonToken!["refresh_token"]!.ToString();
var accessTokenExpiresIn = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + int.Parse(jsonToken!["expires_in"]!.ToString());
await SecureStorage.SetAsync("access_token", accessToken);
await SecureStorage.SetAsync("refresh_token", refreshToken);
Preferences.Set("access_token_epires_in", accessTokenExpiresIn);
}
private async Task RefreshToken()
{
var clientId = DeviceInfo.Current.Platform == DevicePlatform.WinUI ? _windowsClientId : _androidClientId;
var tokenEndpoint = "https://oauth2.googleapis.com/token";
var refreshToken = await SecureStorage.GetAsync("refresh_token");
var client = new HttpClient();
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
{
Content = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", refreshToken!)
]
)
};
var response = await client.SendAsync(tokenRequest);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception($"Error requesting token: {responseBody}");
Debug.WriteLine($"Refresh token: {responseBody}");
var jsonToken = JsonObject.Parse(responseBody);
var accessToken = jsonToken!["access_token"]!.ToString();
var accessTokenExpiresIn = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + int.Parse(jsonToken!["expires_in"]!.ToString());
await SecureStorage.SetAsync("access_token", accessToken);
Preferences.Set("access_token_epires_in", accessTokenExpiresIn);
}
private async Task RevokeTokens()
{
var revokeEndpoint = "https://oauth2.googleapis.com/revoke";
var access_token = await SecureStorage.GetAsync("access_token");
var client = new HttpClient();
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, revokeEndpoint)
{
Content = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("token", access_token!),
]
)
};
var response = await client.SendAsync(tokenRequest);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception($"Error revoking token: {responseBody}");
Debug.WriteLine($"Revoke token: {responseBody}");
SecureStorage.Remove("access_token");
SecureStorage.Remove("refresh_token");
Preferences.Remove("access_token_epires_in");
_credential = null;
_oauth2Service = null;
_driveService = null;
}
private static async Task<string> StartLocalHttpServerAsync(int port)
{
var listener = new HttpListener();
listener.Prefixes.Add($"http://localhost:{port}/");
listener.Start();
Debug.WriteLine($"Listening on http://localhost:{port}/...");
var context = await listener.GetContextAsync();
var code = context.Request.QueryString["code"];
var response = context.Response;
var responseString = "Authorization complete. You can close this window.";
var buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer);
response.OutputStream.Close();
listener.Stop();
if (code is null) throw new Exception("Auth ode not returned");
return code;
}
private static string GenerateCodeVerifier()
{
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[32]; // Length can vary, e.g., 43-128 characters
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
private static string GenerateCodeChallenge(string codeVerifier)
{
var hash = SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier));
return Convert.ToBase64String(hash)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}
Update MainPage.xaml to the following:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="OAuthSample.MainPage"
Loaded="ContentPage_Loaded">
<ScrollView>
<VerticalStackLayout Padding="30,0" Spacing="25">
<Button
x:Name="SignInButton"
Text="Sign In"
Clicked="SignIn_Clicked"
HorizontalOptions="Fill" />
<Button
x:Name="ListButton"
Text="List"
Clicked="List_Clicked"
HorizontalOptions="Fill"
IsVisible="False" />
<Label x:Name="ListLabel" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Update MainPage.xaml.cs to the following:
using OAuthSample.Services;
namespace OAuthSample;
public partial class MainPage : ContentPage
{
readonly GoogleDriveService _googleDriveService = new();
public MainPage()
{
InitializeComponent();
}
private async void ContentPage_Loaded(object sender, EventArgs e)
{
await _googleDriveService.Init();
UpdateButton();
}
private async void SignIn_Clicked(object sender, EventArgs e)
{
if (SignInButton.Text == "Sign In")
{
await _googleDriveService.SignIn();
}
else
{
await _googleDriveService.SignOut();
}
UpdateButton();
}
private async void List_Clicked(object sender, EventArgs e)
{
ListLabel.Text = await _googleDriveService.ListFiles();
}
private void UpdateButton()
{
if (_googleDriveService.IsSignedIn)
{
SignInButton.Text = $"Sign Out ({_googleDriveService.Email})";
ListButton.IsVisible = true;
}
else
{
SignInButton.Text = "Sign In";
ListButton.IsVisible = false;
ListLabel.Text = String.Empty;
}
}
}
(Optional) Control window size for Windows. Update AppShell.xaml.cs to the following:
namespace OAuthSample;
public partial class App : Application
{
public App()
{
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
var displayInfo = DeviceDisplay.Current.MainDisplayInfo;
var width = 700;
var height = 500;
var centerX = (displayInfo.Width / displayInfo.Density - width) / 2;
var centerY = (displayInfo.Height / displayInfo.Density - height) / 2;
return new Window(new AppShell())
{
Width = width,
Height = height,
X = centerX,
Y = centerY
};
}
}
Test on Windows
- Run with Windows Machine profile
- Press Sign In
- Choose account in browser. Note: this account needs to be specified as a test user in OAuth consent screen Test user section (this restriction will be lifted once your app is published).
- Press List
- See files below
Test on Android
- Run with an Android Emulator profile
- Press Sign In
- Choose account in browser. Note: this account needs to be specified as a test user in OAuth consent screen Test user section (this restriction will be lifted once your app is published).
- Press List
- See files below
Github sample app
https://github.com/adiamante/maui.oauth.sample
References
How-To: OAuth2.0 Authentication in NET MAUI using Personal Cloud Providers
#7. OAuth 2.0 | Upload File In Google Drive By API Using Postman | Simple |Upload File Up To 5MB |
Top comments (4)
Hi, I replicated your app just changing the name of the project, everything works fine until it asks me for the account to use, it asks me to accept the app's request but then instead of going back to the app and adding the "list" button it opens Google Chrome and stops like that. Could you please tell me what I need to check again? Thanks
In chrome, did you get to sign in? Maybe step through SignIn_Clicked or GoogleDriveService.SignIn() to see if anything failed.
How to make the same but for YouTube login?
Wouldn't it be the same because you log in through Google? I would think you would just need different scopes. So you need to add the APIs to your Google project then request the corresponding scopes