Skip to content

Commit aadfeb9

Browse files
committed
added basic multiplatform credential handling
1 parent 420ba95 commit aadfeb9

24 files changed

Lines changed: 1177 additions & 34 deletions

.github/workflows/build.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
env:
10+
SOLUTION_PATH: src/Maui.CredentialManagers.slnx
11+
DOTNET_VERSION: '10.0.x'
12+
BUILD_CONFIGURATION: Release
13+
14+
jobs:
15+
build:
16+
runs-on: macos-latest
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Setup .NET
22+
uses: actions/setup-dotnet@v4
23+
with:
24+
dotnet-version: ${{ env.DOTNET_VERSION }}
25+
26+
- name: Install MAUI workload
27+
run: dotnet workload install maui
28+
29+
- name: Restore dependencies
30+
run: dotnet restore ${{ env.SOLUTION_PATH }}
31+
32+
- name: Build
33+
run: dotnet build ${{ env.SOLUTION_PATH }} --no-restore --configuration ${{ env.BUILD_CONFIGURATION }}
34+
35+
- name: Test with coverage
36+
run: dotnet test ${{ env.SOLUTION_PATH }} --no-build --configuration ${{ env.BUILD_CONFIGURATION }} --collect:"XPlat Code Coverage" --results-directory ./coverage
37+
38+
- name: Find coverage file
39+
id: coverage
40+
run: echo "file=$(find ./coverage -name 'coverage.cobertura.xml' | head -1)" >> $GITHUB_OUTPUT
41+
42+
- name: Upload coverage to Codecov
43+
uses: codecov/codecov-action@v5
44+
with:
45+
files: ${{ steps.coverage.outputs.file }}
46+
fail_ci_if_error: false
47+
token: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/publish.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Publish to NuGet
2+
3+
on:
4+
workflow_dispatch:
5+
6+
env:
7+
PROJECT_PATH: src/Maui.CredentialManagers/Maui.CredentialManagers.csproj
8+
SOLUTION_PATH: src/Maui.CredentialManagers.slnx
9+
PACKAGE_NAME: Kebechet.Maui.CredentialManagers
10+
DOTNET_VERSION: '10.0.x'
11+
BUILD_CONFIGURATION: Release
12+
OUTPUT_PATH: ./nupkg
13+
NUGET_SOURCE: https://api.nuget.org/v3/index.json
14+
15+
jobs:
16+
publish:
17+
runs-on: macos-latest
18+
permissions:
19+
contents: write
20+
packages: write
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Setup .NET
26+
uses: actions/setup-dotnet@v4
27+
with:
28+
dotnet-version: ${{ env.DOTNET_VERSION }}
29+
30+
- name: Install MAUI workload
31+
run: dotnet workload install maui
32+
33+
- name: Get version from csproj
34+
id: version
35+
run: |
36+
VERSION=$(grep -oP '(?<=<Version>)[^<]+' ${{ env.PROJECT_PATH }})
37+
echo "version=$VERSION" >> $GITHUB_OUTPUT
38+
echo "Version: $VERSION"
39+
40+
- name: Restore dependencies
41+
run: dotnet restore ${{ env.SOLUTION_PATH }}
42+
43+
- name: Build
44+
run: dotnet build ${{ env.SOLUTION_PATH }} --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore
45+
46+
- name: Pack
47+
run: dotnet pack ${{ env.PROJECT_PATH }} --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --output ${{ env.OUTPUT_PATH }}
48+
49+
- name: Publish to NuGet
50+
run: dotnet nuget push ${{ env.OUTPUT_PATH }}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source ${{ env.NUGET_SOURCE }} --skip-duplicate
51+
52+
- name: Publish to GitHub Packages
53+
run: dotnet nuget push ${{ env.OUTPUT_PATH }}/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --skip-duplicate
54+
55+
- name: Create GitHub Release
56+
uses: softprops/action-gh-release@v2
57+
with:
58+
tag_name: v${{ steps.version.outputs.version }}
59+
name: v${{ steps.version.outputs.version }}
60+
body: |
61+
## ${{ env.PACKAGE_NAME }} v${{ steps.version.outputs.version }}
62+
63+
### Installation
64+
```bash
65+
dotnet add package ${{ env.PACKAGE_NAME }} --version ${{ steps.version.outputs.version }}
66+
```
67+
68+
### Links
69+
- [NuGet Package](https://www.nuget.org/packages/${{ env.PACKAGE_NAME }}/${{ steps.version.outputs.version }})
70+
- [GitHub](https://github.com/Kebechet/Maui.CredentialManagers)
71+
files: ${{ env.OUTPUT_PATH }}/*.nupkg
72+
generate_release_notes: true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,6 @@ FodyWeavers.xsd
399399

400400
# Visual Studio for Mac
401401
**/.DS_Store
402+
403+
# Claude Code
404+
**/.claude/settings.local.json

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2023 Kebechet
3+
Copyright (c) 2023-2026 Kebechet
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,67 @@
1-
# Maui.CredentialManagers
1+
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/kebechet)
2+
3+
# Maui.CredentialManagers
4+
[![NuGet Version](https://img.shields.io/nuget/v/Kebechet.Maui.CredentialManagers)](https://www.nuget.org/packages/Kebechet.Maui.CredentialManagers/)
5+
[![NuGet Downloads](https://img.shields.io/nuget/dt/Kebechet.Maui.CredentialManagers)](https://www.nuget.org/packages/Kebechet.Maui.CredentialManagers/)
6+
[![Build](https://github.com/Kebechet/Maui.CredentialManagers/actions/workflows/build.yml/badge.svg)](https://github.com/Kebechet/Maui.CredentialManagers/actions/workflows/build.yml)
7+
[![codecov](https://codecov.io/gh/Kebechet/Maui.CredentialManagers/graph/badge.svg)](https://codecov.io/gh/Kebechet/Maui.CredentialManagers)
8+
![Last updated](https://img.shields.io/github/last-commit/Kebechet/Maui.CredentialManagers/main?label=last%20updated)
9+
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/samuel_sidor.svg?style=social&label=Follow%20samuel_sidor)](https://x.com/samuel_sidor)
10+
11+
Cross-platform .NET MAUI library for unified credential management on Android and iOS.
12+
13+
## Installation
14+
15+
```bash
16+
dotnet add package Kebechet.Maui.CredentialManagers
17+
```
18+
19+
## Usage
20+
21+
### Registration
22+
23+
```csharp
24+
builder.Services.AddCredentialManagerService(options =>
25+
{
26+
options.GoogleServerClientId = "xxx.apps.googleusercontent.com";
27+
options.GoogleIosClientId = "yyy.apps.googleusercontent.com";
28+
options.GoogleIosRedirectUri = "com.myapp:/oauth2redirect";
29+
options.AppleServiceId = "com.myapp.auth";
30+
options.AppleRedirectUri = "https://myserver.com/auth/apple/callback";
31+
options.AppleAndroidCallbackScheme = "com.myapp:/applecallback";
32+
});
33+
```
34+
35+
### Inject and use
36+
37+
```csharp
38+
@inject ICredentialManagerService CredentialManagerService
39+
40+
// Save password
41+
await CredentialManagerService.CreatePasswordCredential(
42+
new PasswordCredentialDto { Id = "user@example.com", Password = "secret" },
43+
cancellationToken);
44+
45+
// Retrieve password
46+
var result = await CredentialManagerService.GetPasswordCredential(cancellationToken);
47+
48+
// SSO (Google on Android, Apple on iOS by default)
49+
var ssoResult = await CredentialManagerService.ContinueWithSso(
50+
SsoProvider.PlatformDefault, cancellationToken);
51+
52+
// Clear credential state
53+
await CredentialManagerService.ClearCredentialState(cancellationToken);
54+
```
55+
56+
## Supported Platforms
57+
58+
| Feature | Android | iOS |
59+
|---------|---------|-----|
60+
| Password credentials | Credential Manager API | Keychain Services |
61+
| Google Sign-In | Native (Credential Manager) | WebAuthenticator |
62+
| Apple Sign-In | WebAuthenticator | Native (ASAuthorization) |
63+
| Clear credential state | ClearCredentialState API | Keychain removal |
64+
65+
## License
66+
67+
[MIT](LICENSE)

demo/DemoApp/Components/Pages/Home.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
</div>
164164

165165
@code {
166-
private string username = "";
166+
private string username = "test@gmail.com";
167167
private string password = "";
168168
private SsoProvider selectedProvider = SsoProvider.PlatformDefault;
169169

demo/DemoApp/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# DemoApp — Maui.CredentialManagers
2+
3+
A .NET MAUI Blazor app demonstrating the [`Maui.CredentialManagers`](../../src/Maui.CredentialManagers) library.
4+
Targets `net10.0-android` and `net10.0-ios`.
5+
6+
## Features demonstrated
7+
8+
- Save password credentials (Credential Manager on Android, Keychain on iOS)
9+
- Retrieve password credentials (with optional auto-select and filtering)
10+
- SSO login via Google, Apple, or PlatformDefault (Google on Android, Apple on iOS)
11+
- Clear credential state
12+
13+
## Prerequisites
14+
15+
- .NET 10 SDK
16+
- MAUI workload (`dotnet workload install maui`)
17+
- **Android**: Android SDK with API 24+
18+
- **iOS**: Xcode with iOS 16.0+ SDK
19+
20+
## Configuration
21+
22+
All SSO options are set in [`MauiProgram.cs`](MauiProgram.cs) via `AddCredentialManagerService`:
23+
24+
```csharp
25+
builder.Services.AddCredentialManagerService(options =>
26+
{
27+
options.GoogleServerClientId = "your-google-web-client-id";
28+
// options.GoogleIosClientId = "your-google-ios-client-id";
29+
// options.GoogleIosRedirectUri = "your-redirect-uri";
30+
// options.AppleServiceId = "your-apple-service-id";
31+
// options.AppleRedirectUri = "your-apple-redirect-uri";
32+
});
33+
```
34+
35+
### Google SSO
36+
37+
| Option | Description |
38+
|--------|-------------|
39+
| `GoogleServerClientId` | OAuth 2.0 **Web** client ID from Google Cloud Console. This is the client ID your backend uses to verify tokens. Required for native Android sign-in and the Google ID option in `GetPasswordCredential`. |
40+
| `GoogleIosClientId` | iOS-specific Google OAuth client ID (for browser flow on iOS). |
41+
| `GoogleIosRedirectUri` | Redirect URI for iOS browser-based Google auth. |
42+
| `GoogleAndroidRedirectUri` | Redirect URI for Android browser-based Google auth. |
43+
| `GoogleAndroidCallbackScheme` | Custom URL scheme for Android browser callback. |
44+
45+
### Apple SSO
46+
47+
| Option | Description |
48+
|--------|-------------|
49+
| `AppleServiceId` | Service ID from the Apple Developer portal. |
50+
| `AppleRedirectUri` | Redirect URI registered with Apple. |
51+
| `AppleAndroidCallbackScheme` | Callback scheme for Apple auth on Android. |
52+
| `AppleIosCallbackScheme` | Callback scheme for Apple auth on iOS. |
53+
54+
### Auth method defaults
55+
56+
| Provider | Android | iOS |
57+
|----------|---------|-----|
58+
| Google | Native | Browser |
59+
| Apple | Browser | Native |
60+
61+
Override per-platform via `GoogleOnAndroid`, `AppleOnAndroid`, `GoogleOnIos`, `AppleOnIos`. Accepted values: `Native`, `Browser`, `Disabled`.
62+
63+
## Password credentials — no SSO setup needed
64+
65+
Saving and retrieving password credentials works without any SSO configuration. You can test `CreatePasswordCredential`, `GetPasswordCredential`, and `ClearCredentialState` immediately.
66+
67+
## Running
68+
69+
```bash
70+
# Android
71+
dotnet build -f net10.0-android
72+
73+
# iOS
74+
dotnet build -f net10.0-ios
75+
```
76+
77+
## Android-specific notes
78+
79+
- **⚠️ The `<ApplicationId>` in your `.csproj` must exactly match the package name registered in the Google Cloud Console for your OAuth client.** If they don't match, credential requests will silently fail or return empty results with no clear error message. The demo app uses `com.kebechet.demoapp` — make sure the same package name is configured in your Google Cloud project.
80+
- Requires `INTERNET` and `ACCESS_NETWORK_STATE` permissions (already in manifest).
81+
- For passkeys: set up an `assetlinks.json` on your domain and configure Digital Asset Links.
82+
83+
## iOS-specific notes
84+
85+
- Minimum deployment target: iOS 16.0 (required for ASAuthorization passkey APIs).
86+
- For Apple Sign In: enable the **Sign In with Apple** capability in the Apple Developer portal.
87+
- For passkeys: configure **Associated Domains** in your entitlements.

icon.png

59.3 KB
Loading

src/Maui.CredentialManagers/Extensions/IServiceCollectionExtensions.cs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
1-
using Maui.CredentialManagers.Platforms.Android.Services;
1+
using Maui.CredentialManagers.Models.Options;
22
using Maui.CredentialManagers.Services;
3+
#if ANDROID
4+
using Maui.CredentialManagers.Platforms.Android.Services;
5+
#elif IOS
6+
using Maui.CredentialManagers.Platforms.iOS.Services;
7+
#endif
38

49
namespace Maui.CredentialManagers.Extensions;
510

611
public static class IServiceCollectionExtensions
712
{
8-
public static IServiceCollection AddCredentialManagerService(this IServiceCollection services, string googleServerClientId = "")
13+
public static IServiceCollection AddCredentialManagerService(
14+
this IServiceCollection services,
15+
Action<CredentialManagerOptions> configure)
916
{
10-
//#if __ANDROID__
17+
var options = new CredentialManagerOptions();
18+
configure(options);
19+
20+
services.AddSingleton(options);
21+
22+
#if ANDROID
1123
services.AddScoped<CredentialManagerAndroidService>();
12-
services.AddScoped<CredentialManagerService>(provider =>
24+
services.AddScoped<ICredentialManagerService>(provider =>
25+
{
26+
var androidService = provider.GetRequiredService<CredentialManagerAndroidService>();
27+
var opts = provider.GetRequiredService<CredentialManagerOptions>();
28+
return new CredentialManagerService(androidService, opts);
29+
});
30+
#elif IOS
31+
services.AddScoped<CredentialManagerIosService>();
32+
services.AddScoped<ICredentialManagerService>(provider =>
33+
{
34+
var iosService = provider.GetRequiredService<CredentialManagerIosService>();
35+
var opts = provider.GetRequiredService<CredentialManagerOptions>();
36+
return new CredentialManagerService(iosService, opts);
37+
});
38+
#else
39+
services.AddScoped<ICredentialManagerService>(provider =>
1340
{
14-
var dependency = provider.GetRequiredService<CredentialManagerAndroidService>();
15-
return new CredentialManagerService(dependency, googleServerClientId);
41+
var opts = provider.GetRequiredService<CredentialManagerOptions>();
42+
return new CredentialManagerService(opts);
1643
});
17-
//#endif
44+
#endif
1845

1946
return services;
2047
}

0 commit comments

Comments
 (0)