Skip to content

Finalization of package for first release - #4

Merged
Kebechet merged 11 commits into
mainfrom
implemented-multiplatform-support
Mar 12, 2026
Merged

Finalization of package for first release#4
Kebechet merged 11 commits into
mainfrom
implemented-multiplatform-support

Conversation

@Kebechet

Copy link
Copy Markdown
Owner

No description provided.

@Kebechet
Kebechet force-pushed the implemented-multiplatform-support branch from c27fe37 to aadfeb9 Compare March 12, 2026 12:49
@Kebechet
Kebechet marked this pull request as ready for review March 12, 2026 13:54
Copilot AI review requested due to automatic review settings March 12, 2026 13:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Finalizes the library for an initial public release by multi-targeting the MAUI credential manager package, adding a consumer-facing service interface/options model, and including a demo app + CI workflows for build/publish.

Changes:

  • Multi-target the Maui.CredentialManagers library (Android/iOS/MacCatalyst/Windows) and add NuGet packaging metadata/assets.
  • Introduce ICredentialManagerService, CredentialManagerOptions (+ SSO enums/models), and platform-specific partial implementations (Android/iOS + stubs for Windows/MacCatalyst).
  • Add a MAUI Blazor demo app and GitHub Actions workflows for build/coverage and publishing to NuGet/GitHub Packages.

Reviewed changes

Copilot reviewed 45 out of 52 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/Maui.CredentialManagers/Usings.cs Guards AndroidX global usings behind #if ANDROID for multi-targeting.
src/Maui.CredentialManagers/Services/ICredentialManagerService.cs Adds a public cross-platform service interface.
src/Maui.CredentialManagers/Services/CredentialManagerService.cs Refactors to partial class implementing the new interface and using options.
src/Maui.CredentialManagers/Platforms/Android/Services/CredentialManagerService.cs Adds options-driven SSO routing + ClearCredentialState; adjusts ctor and Google client id usage.
src/Maui.CredentialManagers/Platforms/iOS/Services/CredentialManagerService.cs Introduces iOS implementation incl. native Apple sign-in and browser flows.
src/Maui.CredentialManagers/Platforms/iOS/Services/CredentialManagerIosService.cs Adds Keychain helpers + ASAuthorization controller wrappers.
src/Maui.CredentialManagers/Platforms/iOS/Delegates/AuthorizationDelegate.cs Implements ASAuthorization delegate + presentation anchor resolution.
src/Maui.CredentialManagers/Platforms/Windows/Services/CredentialManagerService.cs Adds Windows stub implementation returning “not supported” results.
src/Maui.CredentialManagers/Platforms/MacCatalyst/Services/CredentialManagerService.cs Adds MacCatalyst stub implementation returning “not supported” results.
src/Maui.CredentialManagers/Models/SsoProvider.cs Adds provider selection enum (Google/Apple/PlatformDefault).
src/Maui.CredentialManagers/Models/Options/SsoAuthMethod.cs Adds SSO routing enum (Native/Browser).
src/Maui.CredentialManagers/Models/Options/CredentialManagerOptions.cs Adds shared + platform-specific configuration objects.
src/Maui.CredentialManagers/Models/CredentialDto.cs Extends credential DTO to include Apple ID credentials.
src/Maui.CredentialManagers/Models/AppleIdCredentialDto.cs Adds Apple credential DTO.
src/Maui.CredentialManagers/Extensions/IServiceCollectionExtensions.cs Changes DI registration to accept an options configure callback and registers per-platform services.
src/Maui.CredentialManagers/Maui.CredentialManagers.csproj Adds multi-target frameworks, NuGet metadata, and packs README/LICENSE/icon.
icon.png Adds package icon asset.
demo/DemoApp/DemoApp.csproj Adds MAUI Blazor demo project referencing the library.
demo/DemoApp/MauiProgram.cs Wires up the demo app and registers AddCredentialManagerService.
demo/DemoApp/App.xaml Demo app resources bootstrap.
demo/DemoApp/App.xaml.cs Demo app window creation.
demo/DemoApp/MainPage.xaml Hosts BlazorWebView.
demo/DemoApp/MainPage.xaml.cs Initializes main page.
demo/DemoApp/wwwroot/index.html Demo app host page.
demo/DemoApp/wwwroot/app.css Demo app base styling.
demo/DemoApp/Components/_Imports.razor Razor imports for demo UI.
demo/DemoApp/Components/Routes.razor Blazor router for demo.
demo/DemoApp/Components/Layout/MainLayout.razor Demo layout.
demo/DemoApp/Components/Pages/Home.razor Interactive UI to exercise credential APIs + SSO.
demo/DemoApp/Components/Pages/Home.razor.css Styling for the Home page UI.
demo/DemoApp/Components/Pages/NotFound.razor Demo 404 page.
demo/DemoApp/Platforms/Android/AndroidManifest.xml Adds required network permissions for demo SSO flows.
demo/DemoApp/Platforms/Android/MainActivity.cs Android activity entry point.
demo/DemoApp/Platforms/Android/MainApplication.cs Android application entry point.
demo/DemoApp/Platforms/Android/Resources/values/colors.xml Demo Android theme colors.
demo/DemoApp/Platforms/iOS/AppDelegate.cs iOS app delegate.
demo/DemoApp/Platforms/iOS/Program.cs iOS main entry point.
demo/DemoApp/Platforms/iOS/Info.plist iOS app manifest.
demo/DemoApp/Platforms/iOS/Resources/PrivacyInfo.xcprivacy Adds Apple privacy manifest.
demo/DemoApp/Resources/AppIcon/appicon.svg Demo app icon background.
demo/DemoApp/Resources/AppIcon/appiconfg.svg Demo app icon foreground.
demo/DemoApp/Resources/Splash/splash.svg Demo splash screen.
demo/DemoApp/Resources/Images/dotnet_bot.svg Demo image asset.
demo/DemoApp/Resources/Raw/AboutAssets.txt Demo MAUI raw asset boilerplate.
demo/DemoApp/README.md Demo documentation and configuration notes.
demo/DemoApp.slnx Solution file for demo development/deploy.
README.md Expands project README with install/usage docs and badges.
LICENSE Updates copyright year range.
.gitignore Ignores Claude Code settings file.
.github/workflows/build.yml Adds CI build + test/coverage upload.
.github/workflows/publish.yml Adds manual publish workflow for NuGet/GitHub Packages + GitHub release.
Comments suppressed due to low confidence (1)

src/Maui.CredentialManagers/Platforms/Android/Services/CredentialManagerService.cs:65

  • GetPasswordCredential always builds and adds a GetGoogleIdOption using GoogleServerClientId ?? "". If the client id is missing, this option may throw or cause the credential request to fail unexpectedly. Consider validating GoogleServerClientId before adding the Google option (or only add it when configured) so password-only retrieval still works without Google SSO configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +187 to +197
var state = Guid.NewGuid().ToString();
var nonce = Guid.NewGuid().ToString();
var authUrl = $"https://appleid.apple.com/auth/authorize" +
$"?client_id={Uri.EscapeDataString(_options.AppleServiceId)}" +
$"&redirect_uri={Uri.EscapeDataString(_options.AppleRedirectUri)}" +
$"&response_type=code%20id_token&scope=name%20email" +
$"&response_mode=form_post&state={state}&nonce={nonce}";

var result = await WebAuthenticator.Default.AuthenticateAsync(
new Uri(authUrl), new Uri(_options.Android.AppleCallbackScheme));

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Android Apple browser flow, a state is generated but never validated on return, which leaves the flow vulnerable to CSRF/replay. Also, response_mode=form_post typically won’t be compatible with WebAuthenticator deep-link callbacks. Prefer a response mode supported by custom-scheme redirects and validate the returned state before accepting tokens.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +27 to +31
options.GoogleIosClientId = "yyy.apps.googleusercontent.com";
options.GoogleIosRedirectUri = "com.myapp:/oauth2redirect";
options.AppleServiceId = "com.myapp.auth";
options.AppleRedirectUri = "https://myserver.com/auth/apple/callback";
options.AppleAndroidCallbackScheme = "com.myapp:/applecallback";

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage snippet references option properties that don’t exist in CredentialManagerOptions (e.g., GoogleIosClientId, GoogleIosRedirectUri, AppleAndroidCallbackScheme). The current API uses nested platform options (options.Ios.GoogleClientId, options.Ios.GoogleRedirectUri, options.Android.AppleCallbackScheme, etc.). Update the README snippet to match the actual option names so consumers can copy/paste it successfully.

Suggested change
options.GoogleIosClientId = "yyy.apps.googleusercontent.com";
options.GoogleIosRedirectUri = "com.myapp:/oauth2redirect";
options.AppleServiceId = "com.myapp.auth";
options.AppleRedirectUri = "https://myserver.com/auth/apple/callback";
options.AppleAndroidCallbackScheme = "com.myapp:/applecallback";
options.Ios.GoogleClientId = "yyy.apps.googleusercontent.com";
options.Ios.GoogleRedirectUri = "com.myapp:/oauth2redirect";
options.Apple.ServiceId = "com.myapp.auth";
options.Apple.RedirectUri = "https://myserver.com/auth/apple/callback";
options.Android.AppleCallbackScheme = "com.myapp:/applecallback";

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +14
/// <summary>
/// Stores a password credential in the platform's credential manager.
/// On Android uses Credential Manager API, on iOS uses Keychain Services.
/// </summary>

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs for CreatePasswordCredential say “on iOS uses Keychain Services”, but the iOS GetPasswordCredential implementation uses ASAuthorizationPasswordProvider (not the app Keychain entries created via SecKeyChain). Consider updating the summary to clarify what store/retrieve mechanisms are actually used on iOS so callers don’t assume create/get are symmetric there.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +41
/// Initiates a Single Sign-On flow with the specified provider.
/// The auth method (native SDK or browser) is determined by <see cref="CredentialManagerOptions"/> settings.
/// <see cref="SsoProvider.PlatformDefault"/> resolves to Google on Android and Apple on iOS.

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs state the SSO auth method is determined by CredentialManagerOptions, but some methods are currently fixed by platform (e.g., Google on iOS is always browser; Apple on Android is always browser). Consider clarifying this (or making the options actually affect all branches) so callers know which settings are honored.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +26
var status = _credentialManagerIosService.StorePassword(
_options.Ios.GoogleRedirectUri ?? "default",
passwordCredential.Id,
passwordCredential.Password);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On iOS, CreatePasswordCredential stores the password in the app Keychain using Ios.GoogleRedirectUri (or the literal "default") as the SecRecord.Server. This is unrelated to the credential being saved and will also not match what GetPasswordCredential retrieves (it uses ASAuthorizationPasswordProvider, not your app Keychain entries), so saved credentials likely won’t be retrievable via this API. Consider either (1) using a stable, app-specific server identifier (e.g., bundle id / configured relying-party/domain) and retrieving from Keychain in GetPasswordCredential, or (2) removing the Keychain write and relying solely on the platform password/AutoFill APIs for both create and get.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +93
public SsoAuthMethod GoogleAuthMethod { get; set; } = SsoAuthMethod.Native;

/// <summary>
/// How Apple SSO is handled on Android. Defaults to <see cref="SsoAuthMethod.Browser"/>.
/// Native Apple Sign-In is not supported on Android.
/// </summary>
public SsoAuthMethod AppleAuthMethod { get; } = SsoAuthMethod.Browser;
}

/// <summary>
/// iOS-specific SSO configuration.
/// </summary>
public class IosCredentialOptions
{
/// <summary>
/// iOS-specific Google OAuth client ID for browser-based Google authentication.
/// </summary>
public string? GoogleClientId { get; set; }

/// <summary>
/// Redirect URI for browser-based Google authentication on iOS.
/// </summary>
public string? GoogleRedirectUri { get; set; }

/// <summary>
/// Custom URL scheme used to capture the browser callback for Apple auth on iOS.
/// </summary>
public string? AppleCallbackScheme { get; set; }

/// <summary>
/// How Google SSO is handled on iOS. Defaults to <see cref="SsoAuthMethod.Browser"/>.
/// Native Google Sign-In is not supported on iOS.
/// </summary>
public SsoAuthMethod GoogleAuthMethod { get; } = SsoAuthMethod.Browser;

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AndroidCredentialOptions.AppleAuthMethod and IosCredentialOptions.GoogleAuthMethod are declared as get-only properties, but the public README/demo docs suggest these can be overridden via configuration. Either make these settable or update the docs to reflect that they are fixed to Browser.

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +147
var nonce = Guid.NewGuid().ToString();
var authUrl = $"https://accounts.google.com/o/oauth2/v2/auth" +
$"?client_id={Uri.EscapeDataString(_options.GoogleServerClientId)}" +
$"&redirect_uri={Uri.EscapeDataString(_options.Android.GoogleRedirectUri)}" +
$"&response_type=code&scope=openid%20email%20profile&nonce={nonce}";

var result = await WebAuthenticator.Default.AuthenticateAsync(
new Uri(authUrl), new Uri(_options.Android.GoogleCallbackScheme));

var idToken = result.IdToken ?? result.AccessToken;
if (string.IsNullOrEmpty(idToken))

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google browser sign-in on Android uses response_type=code but then treats result.IdToken ?? result.AccessToken as an ID token. In the auth-code flow, WebAuthenticator will return a code (usually result.Properties["code"]), not an ID token/access token, so this will fail or return the wrong data. Either request an ID token directly or implement a proper code exchange (ideally with PKCE) and return the resulting id_token.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +47

- name: Test with coverage
run: dotnet test ${{ env.SOLUTION_PATH }} --no-build --configuration ${{ env.BUILD_CONFIGURATION }} --collect:"XPlat Code Coverage" --results-directory ./coverage

- name: Find coverage file
id: coverage
run: echo "file=$(find ./coverage -name 'coverage.cobertura.xml' | head -1)" >> $GITHUB_OUTPUT

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ${{ steps.coverage.outputs.file }}
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow runs dotnet test on src/Maui.CredentialManagers.slnx, but that solution currently only contains the library project (no test project / Microsoft.NET.Test.Sdk). dotnet test will fail or produce no coverage in this setup. Either add a proper test project to the solution, or remove the test/coverage steps (and Codecov upload) to keep CI green.

Suggested change
- name: Test with coverage
run: dotnet test ${{ env.SOLUTION_PATH }} --no-build --configuration ${{ env.BUILD_CONFIGURATION }} --collect:"XPlat Code Coverage" --results-directory ./coverage
- name: Find coverage file
id: coverage
run: echo "file=$(find ./coverage -name 'coverage.cobertura.xml' | head -1)" >> $GITHUB_OUTPUT
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ${{ steps.coverage.outputs.file }}
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/publish.yml Outdated
- name: Get version from csproj
id: version
run: |
VERSION=$(grep -oP '(?<=<Version>)[^<]+' ${{ env.PROJECT_PATH }})

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macOS runners, the default grep is BSD grep and does not support -P (PCRE). The Get version from csproj step will fail. Consider extracting the version via dotnet msbuild -getProperty:Version, xmllint, or a small PowerShell/Python snippet instead of grep -oP.

Suggested change
VERSION=$(grep -oP '(?<=<Version>)[^<]+' ${{ env.PROJECT_PATH }})
VERSION=$(dotnet msbuild ${{ env.PROJECT_PATH }} -nologo -getProperty:Version | tr -d '[:space:]')

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +184
var state = Guid.NewGuid().ToString();
var nonce = Guid.NewGuid().ToString();
var authUrl = $"https://appleid.apple.com/auth/authorize" +
$"?client_id={Uri.EscapeDataString(_options.AppleServiceId)}" +
$"&redirect_uri={Uri.EscapeDataString(_options.AppleRedirectUri)}" +
$"&response_type=code%20id_token&scope=name%20email" +
$"&response_mode=form_post&state={state}&nonce={nonce}";

var result = await WebAuthenticator.Default.AuthenticateAsync(
new Uri(authUrl), new Uri(_options.Ios.AppleCallbackScheme));

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Apple browser flow builds an authorization URL with response_mode=form_post and generates a state, but the returned state is never validated. form_post responses also typically won’t be returned to a custom-scheme callback in a way WebAuthenticator can parse. Prefer a response mode compatible with deep-link callbacks (and validate the returned state against the generated value) to avoid both broken sign-in and CSRF risks.

Copilot uses AI. Check for mistakes.
@Kebechet
Kebechet merged commit 99773bd into main Mar 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants