Finalization of package for first release - #4
Conversation
c27fe37 to
aadfeb9
Compare
There was a problem hiding this comment.
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.CredentialManagerslibrary (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
GetPasswordCredentialalways builds and adds aGetGoogleIdOptionusingGoogleServerClientId ?? "". If the client id is missing, this option may throw or cause the credential request to fail unexpectedly. Consider validatingGoogleServerClientIdbefore 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.
| 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)); | ||
|
|
There was a problem hiding this comment.
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.
| 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"; |
There was a problem hiding this comment.
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.
| 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"; |
| /// <summary> | ||
| /// Stores a password credential in the platform's credential manager. | ||
| /// On Android uses Credential Manager API, on iOS uses Keychain Services. | ||
| /// </summary> |
There was a problem hiding this comment.
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.
| /// 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. |
There was a problem hiding this comment.
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.
| var status = _credentialManagerIosService.StorePassword( | ||
| _options.Ios.GoogleRedirectUri ?? "default", | ||
| passwordCredential.Id, | ||
| passwordCredential.Password); |
There was a problem hiding this comment.
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.
| 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; | ||
|
|
There was a problem hiding this comment.
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.
| 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)) |
There was a problem hiding this comment.
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.
|
|
||
| - 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 }} |
There was a problem hiding this comment.
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.
| - 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 }} |
| - name: Get version from csproj | ||
| id: version | ||
| run: | | ||
| VERSION=$(grep -oP '(?<=<Version>)[^<]+' ${{ env.PROJECT_PATH }}) |
There was a problem hiding this comment.
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.
| VERSION=$(grep -oP '(?<=<Version>)[^<]+' ${{ env.PROJECT_PATH }}) | |
| VERSION=$(dotnet msbuild ${{ env.PROJECT_PATH }} -nologo -getProperty:Version | tr -d '[:space:]') |
| 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)); |
There was a problem hiding this comment.
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.
No description provided.