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 (0)