Lädt...


🔧 .NET MAUI Google Drive OAuth on Windows and Android


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

Google Cloud Console

  • Create project if it doesn't exist yet
  • Select your project

Select/New project

  • Add Google Drive API

APIs & Services link

Enable APIs & Services link

Search google drive API

  • Set up OAuth consent screen

OAuth consent screen link

  • Make sure to add your test user

Add test user

  • Create Credentials

Create Credentials link

  • Windows OAuth client ID
    • choose Universal Windows Platform (UWP)
    • set Store ID to test. Will need to be different for a real app

Windows OAuth Client ID

  • 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

Android OAuth Client ID

.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

Windows Machine profile

  • Press Sign In

Windows 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).

Choose account windows

Allow access windows

Authentication complete windows

  • Press List

Press list windows

  • See files below

See files windows

Test on Android

  • Run with an Android Emulator profile

Android profile

  • Press Sign In

Android 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).

Choose account android

Allow access android

  • Press List

Press list android

  • See files below

See files android

Github sample app

https://github.com/adiamante/maui.oauth.sample

References

Google Identity documentation

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 |

...

🔧 .NET MAUI Google Drive OAuth on Windows and Android


📈 45.9 Punkte
🔧 Programmierung

🎥 .NET MAUI Community Standup - .NET Conf Recap and .NET MAUI Updates


📈 45.64 Punkte
🎥 Video | Youtube

🎥 .NET MAUI Community Standup - .NET MAUI LAUNCH PARTY!!!!


📈 39.12 Punkte
🎥 Video | Youtube

🎥 .NET MAUI Community Standup - Get to MAUI with the .NET Upgrade Assistant feat. Sweeky!


📈 39.12 Punkte
🎥 Video | Youtube

🔧 Why Use XAML and MAUI? (1 of 18) | Building Apps with XAML and .NET MAUI


📈 36.28 Punkte
🔧 Programmierung

🕵️ CVE-2025-22376 | Net::OAuth up to 0.28 on Perl Net::OAuth::Client weak prng


📈 35.98 Punkte
🕵️ Sicherheitslücken

🎥 .NET MAUI Community Standup - It's MAUI month! Stories from migrating the Xamarin Community Toolkit


📈 33.83 Punkte
🎥 Video | Youtube

🎥 .NET MAUI Community Standup - One month till MAUI! It's go time!


📈 33.83 Punkte
🎥 Video | Youtube

🐧 Name clash with KDE MAUI toolkit by Microsoft MAUI toolkit


📈 28.54 Punkte
🐧 Linux Tipps

🐧 dotnet/maui - Stop Stealing KDE MAUI's name


📈 28.54 Punkte
🐧 Linux Tipps

🔧 MAUI - Por onde começar? Parte 1: Entendendo o MAUI


📈 28.54 Punkte
🔧 Programmierung

🎥 Enhancing .NET MAUI: Quality Performance and Interoperability in .NET 9 | OD534


📈 26.08 Punkte
🎥 Video | Youtube

🕵️ Openshift OAuth Server /oauth/token/request Request cross site scripting


📈 25.4 Punkte
🕵️ Sicherheitslücken

🔧 Improving user safety in OAuth flows through new OAuth Custom URI scheme restrictions


📈 25.4 Punkte
🔧 Programmierung

🔧 OAuth 2.0 Explained: The Complete Guide to Understanding OAuth


📈 25.4 Punkte
🔧 Programmierung

🎥 Xamarin: .NET Community Standup - June 11th 2020 - Build 2020 Recap + .NET MAUI


📈 24.85 Punkte
🎥 Video | Youtube

🔧 A Journey to .NET MAUI | On .NET


📈 24.85 Punkte
🔧 Programmierung

🎥 .NET MAUI Community Standup - Dmitry Lyalin warms us up to .NET Hot Reload. Plus: Sweeky!


📈 24.85 Punkte
🎥 Video | Youtube

📰 Anfang August: Neue .NET Conf mit Fokus auf .NET MAUI angekündigt


📈 24.85 Punkte
📰 IT Nachrichten

🎥 Welcome to .NET MAUI (Multi-platform App UI) | #SamosaChai.NET


📈 24.85 Punkte
🎥 Video | Youtube

📰 Die wichtigsten News der Kalenderwoche 45/2022: .NET 7, .NET MAUI, Visual Studio und mehr


📈 24.85 Punkte
📰 IT Nachrichten

📰 Cross-Plattform: .NET MAUI Community Toolkit passt sich an .NET 7 an


📈 24.85 Punkte
📰 IT Nachrichten

🔧 .NET Meteor Update - New Level of Productivity for .NET MAUI in VS Code


📈 24.85 Punkte
🔧 Programmierung

matomo