├── .github
├── FUNDING.yml
└── workflows
│ ├── publish-release.yml
│ ├── pr.yml
│ ├── start-release.yml
│ └── ci.yml
├── avantipoint-icon.png
├── sample
├── DemoMobileApp
│ ├── Resources
│ │ ├── Fonts
│ │ │ ├── OpenSans-Regular.ttf
│ │ │ └── OpenSans-Semibold.ttf
│ │ ├── AppIcon
│ │ │ ├── appicon.svg
│ │ │ └── appiconfg.svg
│ │ ├── Raw
│ │ │ └── AboutAssets.txt
│ │ ├── Splash
│ │ │ └── splash.svg
│ │ ├── Styles
│ │ │ ├── Colors.xaml
│ │ │ └── Styles.xaml
│ │ └── Images
│ │ │ └── dotnet_bot.svg
│ ├── AppShell.xaml.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Platforms
│ │ ├── iOS
│ │ │ ├── AppDelegate.cs
│ │ │ ├── Program.cs
│ │ │ └── Info.plist
│ │ ├── Android
│ │ │ ├── Resources
│ │ │ │ └── values
│ │ │ │ │ └── colors.xml
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── MainApplication.cs
│ │ │ ├── MainActivity.cs
│ │ │ └── WebAuthenticatorActivity.cs
│ │ ├── MacCatalyst
│ │ │ ├── AppDelegate.cs
│ │ │ ├── Program.cs
│ │ │ └── Info.plist
│ │ ├── Windows
│ │ │ ├── App.xaml
│ │ │ ├── app.manifest
│ │ │ ├── App.xaml.cs
│ │ │ └── Package.appxmanifest
│ │ └── Tizen
│ │ │ ├── Main.cs
│ │ │ └── tizen-manifest.xml
│ ├── MainPage.xaml.cs
│ ├── Services
│ │ └── IUserProfileService.cs
│ ├── AppShell.xaml
│ ├── Converters
│ │ └── IsAuthenticatedConverter.cs
│ ├── MauiProgram.cs
│ ├── ViewModels
│ │ └── MainPageViewModel.cs
│ ├── MainPage.xaml
│ └── DemoMobileApp.csproj
└── DemoAPI
│ ├── Data
│ ├── AuthorizedTokens.cs
│ ├── UserRole.cs
│ └── UserContext.cs
│ ├── web.config
│ ├── appsettings.json
│ ├── DemoAPI.csproj
│ ├── Properties
│ └── launchSettings.json
│ ├── CustomClaimsHandler.cs
│ └── Program.cs
├── src
└── AvantiPoint.MobileAuth
│ ├── build
│ └── AvantiPoint.MobileAuth.props
│ ├── Authentication
│ ├── ITokenOptions.cs
│ ├── ITokenService.cs
│ ├── AuthenticationExtensions.cs
│ ├── MobileJwtValidationHandler.cs
│ └── TokenService.cs
│ ├── Stores
│ ├── GeneratedToken.cs
│ ├── ITokenStore.cs
│ └── TokenStore.cs
│ ├── IMobileAuthClaimsHandler.cs
│ ├── Configuration
│ ├── OAuthProviderOptions.cs
│ ├── GoogleProviderOptions.cs
│ ├── MicrosoftProviderOptions.cs
│ ├── OAuthLibraryOptions.cs
│ └── AppleOAuthOptions.cs
│ ├── Http
│ └── HttpContextHelpers.cs
│ ├── AvantiPoint.MobileAuth.csproj
│ ├── MobileAuthenticationBuilder.cs
│ ├── MobileAuthClaimsHandler.cs
│ └── MobileAuth.cs
├── version.json
├── LICENSE
├── Directory.Build.props
├── AvantiPoint.MobileAuth.sln
├── ReadMe.md
└── .gitignore
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [dansiegel]
2 | custom: ["https://www.paypal.me/dansiegel"]
--------------------------------------------------------------------------------
/avantipoint-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvantiPoint/mobileauth-lib/HEAD/avantipoint-icon.png
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/Fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvantiPoint/mobileauth-lib/HEAD/sample/DemoMobileApp/Resources/Fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/Fonts/OpenSans-Semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvantiPoint/mobileauth-lib/HEAD/sample/DemoMobileApp/Resources/Fonts/OpenSans-Semibold.ttf
--------------------------------------------------------------------------------
/sample/DemoMobileApp/AppShell.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace DemoMobileApp;
2 |
3 | public partial class AppShell : Shell
4 | {
5 | public AppShell()
6 | {
7 | InitializeComponent();
8 | }
9 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Windows Machine": {
4 | "commandName": "MsixPackage",
5 | "nativeDebugging": false
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/sample/DemoAPI/Data/AuthorizedTokens.cs:
--------------------------------------------------------------------------------
1 | namespace DemoAPI.Data;
2 |
3 | public class AuthorizedTokens
4 | {
5 | public Guid Id { get; set; }
6 |
7 | public string Token { get; set; } = default!;
8 | }
9 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/build/AvantiPoint.MobileAuth.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Authentication/ITokenOptions.cs:
--------------------------------------------------------------------------------
1 | namespace AvantiPoint.MobileAuth.Authentication;
2 |
3 | public interface ITokenOptions
4 | {
5 | string? JwtKey { get; }
6 | bool OverrideTokenExpiration { get; }
7 | TimeSpan DefaultExpiration { get; }
8 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/iOS/AppDelegate.cs:
--------------------------------------------------------------------------------
1 | using Foundation;
2 |
3 | namespace DemoMobileApp;
4 |
5 | [Register("AppDelegate")]
6 | public class AppDelegate : MauiUIApplicationDelegate
7 | {
8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
9 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Android/Resources/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #512BD4
4 | #2B0B98
5 | #2B0B98
6 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/AppIcon/appicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Stores/GeneratedToken.cs:
--------------------------------------------------------------------------------
1 | namespace AvantiPoint.MobileAuth.Stores;
2 |
3 | public class GeneratedToken
4 | {
5 | public string Token { get; set; } = default!;
6 | public DateTimeOffset Expires { get; set; }
7 | public bool Revoked { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/MacCatalyst/AppDelegate.cs:
--------------------------------------------------------------------------------
1 | using Foundation;
2 |
3 | namespace DemoMobileApp;
4 |
5 | [Register("AppDelegate")]
6 | public class AppDelegate : MauiUIApplicationDelegate
7 | {
8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
9 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/MainPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using DemoMobileApp.ViewModels;
2 |
3 | namespace DemoMobileApp;
4 |
5 | public partial class MainPage : ContentPage
6 | {
7 | public MainPage(MainPageViewModel viewModel)
8 | {
9 | InitializeComponent();
10 | BindingContext = viewModel;
11 | }
12 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Services/IUserProfileService.cs:
--------------------------------------------------------------------------------
1 | using Refit;
2 |
3 | namespace DemoMobileApp.Services;
4 |
5 | public interface IUserProfileService
6 | {
7 | [Headers("Authorization: Bearer")]
8 | [Get("/profile")]
9 | Task>> GetProfileClaims();
10 | }
11 |
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
3 | "version": "0.2",
4 | "assemblyVersion": {
5 | "precision": "revision"
6 | },
7 | "publicReleaseRefSpec": [
8 | "^refs/heads/master$"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: Publish MobileAuth Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | publish-release:
9 | uses: avantipoint/workflow-templates/.github/workflows/deploy-nuget-from-release.yml@master
10 | secrets:
11 | apiKey: ${{ secrets.NUGET_API_KEY }}
12 |
--------------------------------------------------------------------------------
/sample/DemoAPI/Data/UserRole.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace DemoAPI.Data;
4 |
5 | public class UserRole
6 | {
7 | public Guid Id { get; set; }
8 |
9 | [EmailAddress]
10 | public string Email { get; set; } = default!;
11 | public string Role { get; set; } = default!;
12 | }
13 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Stores/ITokenStore.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace AvantiPoint.MobileAuth.Stores;
4 |
5 | public interface ITokenStore
6 | {
7 | ValueTask AddToken(string jwt, DateTimeOffset expires);
8 | ValueTask TokenExists(string jwt);
9 | ValueTask RemoveToken(string jwt);
10 | }
11 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Windows/App.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/DemoAPI/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/IMobileAuthClaimsHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using Microsoft.AspNetCore.Authentication;
3 | using Microsoft.AspNetCore.Http;
4 |
5 | namespace AvantiPoint.MobileAuth;
6 |
7 | public interface IMobileAuthClaimsHandler
8 | {
9 | ValueTask> GenerateClaims(HttpContext context, AuthenticateResult auth, string scheme);
10 | }
11 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Configuration/OAuthProviderOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 |
3 | namespace AvantiPoint.MobileAuth.Configuration;
4 |
5 | internal abstract class OAuthProviderOptions
6 | {
7 | public string? ClientId { get; set; }
8 |
9 | public string? ClientSecret { get; set; }
10 |
11 | public abstract void Configure(AuthenticationBuilder builder);
12 | }
13 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Tizen/Main.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Maui;
3 | using Microsoft.Maui.Hosting;
4 |
5 | namespace DemoMobileApp;
6 |
7 | internal class Program : MauiApplication
8 | {
9 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
10 |
11 | static void Main(string[] args)
12 | {
13 | var app = new Program();
14 | app.Run(args);
15 | }
16 | }
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Authentication/ITokenService.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using Microsoft.IdentityModel.Tokens;
3 |
4 | namespace AvantiPoint.MobileAuth.Authentication;
5 |
6 | public interface ITokenService
7 | {
8 | ValueTask BuildToken(IEnumerable claims);
9 | ValueTask IsTokenValid(string token);
10 | ValueTask InvalidateToken(string token);
11 | SymmetricSecurityKey GetKey();
12 | }
13 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/iOS/Program.cs:
--------------------------------------------------------------------------------
1 | using ObjCRuntime;
2 | using UIKit;
3 |
4 | namespace DemoMobileApp;
5 |
6 | public class Program
7 | {
8 | // This is the main entry point of the application.
9 | static void Main(string[] args)
10 | {
11 | // if you want to use a different Application Delegate class from "AppDelegate"
12 | // you can specify it here.
13 | UIApplication.Main(args, null, typeof(AppDelegate));
14 | }
15 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Android/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/MacCatalyst/Program.cs:
--------------------------------------------------------------------------------
1 | using ObjCRuntime;
2 | using UIKit;
3 |
4 | namespace DemoMobileApp;
5 |
6 | public class Program
7 | {
8 | // This is the main entry point of the application.
9 | static void Main(string[] args)
10 | {
11 | // if you want to use a different Application Delegate class from "AppDelegate"
12 | // you can specify it here.
13 | UIApplication.Main(args, null, typeof(AppDelegate));
14 | }
15 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Android/MainApplication.cs:
--------------------------------------------------------------------------------
1 | using Android.App;
2 | using Android.Runtime;
3 |
4 | namespace DemoMobileApp
5 | {
6 | [Application]
7 | public class MainApplication : MauiApplication
8 | {
9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership)
10 | : base(handle, ownership)
11 | {
12 | }
13 |
14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
15 | }
16 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Android/MainActivity.cs:
--------------------------------------------------------------------------------
1 | using Android.App;
2 | using Android.Content.PM;
3 | using Android.OS;
4 |
5 | namespace DemoMobileApp;
6 |
7 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
8 | public class MainActivity : MauiAppCompatActivity
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/AppShell.xaml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Authentication/AuthenticationExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 |
3 | namespace AvantiPoint.MobileAuth.Authentication;
4 |
5 | internal static class AuthenticationExtensions
6 | {
7 | public static bool ContainsKey(this IEnumerable claims, string type) =>
8 | claims.Any(x => x.Type == type);
9 |
10 | public static string? FindFirstValue(this IEnumerable claims, string type) =>
11 | claims.FirstOrDefault(x => x.Type == type)?.Value;
12 | }
13 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Android/WebAuthenticatorActivity.cs:
--------------------------------------------------------------------------------
1 | using Android.App;
2 | using Android.Content;
3 | using Android.Content.PM;
4 |
5 | namespace DemoMobileApp;
6 |
7 | [Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
8 | [IntentFilter(new[] { Intent.ActionView },
9 | Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
10 | DataScheme = Constants.CallbackScheme)]
11 | public class WebAuthenticatorActivity : WebAuthenticatorCallbackActivity
12 | {
13 | }
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: Mobile Auth PR Validation
2 |
3 | on:
4 | pull_request:
5 | branches: [ master ]
6 | paths:
7 | - "*.props"
8 | - "sample/**"
9 | - "src/**"
10 | - "version.json"
11 | - ".github/workflows/ci.yml"
12 |
13 | jobs:
14 | build:
15 | uses: avantipoint/workflow-templates/.github/workflows/dotnet-build.yml@master
16 | permissions:
17 | statuses: write
18 | checks: write
19 | with:
20 | name: Mobile Auth
21 | solution-path: build.slnf
22 | run-tests: false
--------------------------------------------------------------------------------
/sample/DemoAPI/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*",
9 | "OAuth": {
10 | "CallbackScheme": "myapp-scheme",
11 | "JwtKey": "DanSiegelMakesTheBestLibraries",
12 | "Apple": {
13 | "ServiceId": "",
14 | "KeyId": "",
15 | "TeamId": ""
16 | },
17 | "Google": {
18 | "ClientId": "",
19 | "ClientSecret": ""
20 | },
21 | "Microsoft": {
22 | "ClientId": "",
23 | "ClientSecret": ""
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/DemoAPI/DemoAPI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Configuration/GoogleProviderOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace AvantiPoint.MobileAuth.Configuration;
5 |
6 | internal sealed class GoogleProviderOptions : OAuthProviderOptions
7 | {
8 | public override void Configure(AuthenticationBuilder builder)
9 | {
10 | if (string.IsNullOrEmpty(ClientId) || string.IsNullOrEmpty(ClientSecret))
11 | return;
12 |
13 | builder.AddGoogle(options =>
14 | {
15 | options.ClientId = ClientId;
16 | options.ClientSecret = ClientSecret;
17 | options.SaveTokens = true;
18 | });
19 | }
20 | }
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Configuration/MicrosoftProviderOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace AvantiPoint.MobileAuth.Configuration;
5 |
6 | internal sealed class MicrosoftProviderOptions : OAuthProviderOptions
7 | {
8 | public override void Configure(AuthenticationBuilder builder)
9 | {
10 | if (string.IsNullOrEmpty(ClientId) || string.IsNullOrEmpty(ClientSecret))
11 | return;
12 |
13 | builder.AddMicrosoftAccount(options =>
14 | {
15 | options.ClientId = ClientId;
16 | options.ClientSecret = ClientSecret;
17 | options.SaveTokens = true;
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/Raw/AboutAssets.txt:
--------------------------------------------------------------------------------
1 | Any raw assets you want to be deployed with your application can be placed in
2 | this directory (and child directories). Deployment of the asset to your application
3 | is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
4 |
5 |
6 |
7 | These files will be deployed with you package and will be accessible using Essentials:
8 |
9 | async Task LoadMauiAsset()
10 | {
11 | using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
12 | using var reader = new StreamReader(stream);
13 |
14 | var contents = reader.ReadToEnd();
15 | }
16 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Converters/IsAuthenticatedConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 |
3 | namespace DemoMobileApp.Converters;
4 |
5 | public class IsAuthenticatedConverter : IValueConverter
6 | {
7 | public bool Invert { get; set; }
8 |
9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
10 | {
11 | if (value is not IEnumerable claims)
12 | return GetValue(false);
13 |
14 | return GetValue(claims.Any());
15 | }
16 |
17 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
18 | {
19 | throw new NotImplementedException();
20 | }
21 |
22 | private bool GetValue(bool value) =>
23 | Invert ? !value : value;
24 | }
25 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Tizen/tizen-manifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | appicon.xhigh.png
7 |
8 |
9 |
10 |
11 | http://tizen.org/privilege/internet
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Http/HttpContextHelpers.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 |
3 | namespace AvantiPoint.MobileAuth.Http;
4 |
5 | internal static class HttpContextHelpers
6 | {
7 | public static Task StatusCode(this HttpContext context, int statusCode)
8 | {
9 | context.Response.StatusCode = statusCode;
10 | return context.Response.Body.FlushAsync();
11 | }
12 |
13 | public static Task Ok(this HttpContext context)
14 | => context.StatusCode(StatusCodes.Status200OK);
15 |
16 | public static Task BadRequest(this HttpContext context)
17 | => context.StatusCode(StatusCodes.Status400BadRequest);
18 |
19 | public static Task NoContent(this HttpContext context)
20 | => context.StatusCode(StatusCodes.Status204NoContent);
21 | }
22 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Windows/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 | true/PM
12 | PerMonitorV2, PerMonitor
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Windows/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.UI.Xaml;
2 |
3 | // To learn more about WinUI, the WinUI project structure,
4 | // and more about our project templates, see: http://aka.ms/winui-project-info.
5 |
6 | namespace DemoMobileApp.WinUI;
7 |
8 | ///
9 | /// Provides application-specific behavior to supplement the default Application class.
10 | ///
11 | public partial class App : MauiWinUIApplication
12 | {
13 | ///
14 | /// Initializes the singleton application object. This is the first line of authored code
15 | /// executed, and as such is the logical equivalent of main() or WinMain().
16 | ///
17 | public App()
18 | {
19 | this.InitializeComponent();
20 | }
21 |
22 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
23 | }
--------------------------------------------------------------------------------
/sample/DemoAPI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:38193",
8 | "sslPort": 44357
9 | }
10 | },
11 | "profiles": {
12 | "DemoAPI": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:7172;http://localhost:5172",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "IIS Express": {
23 | "commandName": "IISExpress",
24 | "launchBrowser": true,
25 | "launchUrl": "swagger",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/start-release.yml:
--------------------------------------------------------------------------------
1 | name: Start NuGet Release
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | build:
8 | uses: avantipoint/workflow-templates/.github/workflows/dotnet-build.yml@master
9 | permissions:
10 | statuses: write
11 | checks: write
12 | with:
13 | name: Mobile Auth
14 | solution-path: build.slnf
15 | run-tests: false
16 | code-sign: true
17 | secrets:
18 | codeSignKeyVault: ${{ secrets.CodeSignKeyVault }}
19 | codeSignClientId: ${{ secrets.CodeSignClientId }}
20 | codeSignTenantId: ${{ secrets.CodeSignTenantId }}
21 | codeSignClientSecret: ${{ secrets.CodeSignClientSecret }}
22 | codeSignCertificate: ${{ secrets.CodeSignCertificate }}
23 |
24 | release:
25 | uses: avantipoint/workflow-templates/.github/workflows/generate-release.yml@master
26 | needs: [build]
27 | permissions:
28 | contents: write
29 | with:
30 | package-name: AvantiPoint.MobileAuth
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 AvantiPoint, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/sample/DemoAPI/Data/UserContext.cs:
--------------------------------------------------------------------------------
1 | using AvantiPoint.MobileAuth.Stores;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace DemoAPI.Data;
5 |
6 | public class UserContext : DbContext, ITokenStore
7 | {
8 | public UserContext(DbContextOptions options)
9 | : base(options)
10 | {
11 | }
12 |
13 | public DbSet AuthorizedTokens { get; set; }
14 |
15 | public DbSet UserRoles { get; set; }
16 |
17 | public async ValueTask AddToken(string jwt, DateTimeOffset expires)
18 | {
19 | await AuthorizedTokens.AddAsync(new Data.AuthorizedTokens()
20 | {
21 | Token = jwt
22 | });
23 | await SaveChangesAsync();
24 | }
25 |
26 | public async ValueTask RemoveToken(string jwt)
27 | {
28 | var tokens = await AuthorizedTokens.Where(x => x.Token == jwt).ToArrayAsync();
29 | if(tokens.Any())
30 | {
31 | AuthorizedTokens.RemoveRange(tokens);
32 | await SaveChangesAsync();
33 | }
34 | }
35 |
36 | public async ValueTask TokenExists(string jwt) =>
37 | await AuthorizedTokens.AnyAsync(x => x.Token == jwt);
38 | }
39 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Stores/TokenStore.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.VisualBasic;
3 |
4 | namespace AvantiPoint.MobileAuth.Stores;
5 |
6 | internal class TokenStore : DbContext, ITokenStore
7 | {
8 | public TokenStore(DbContextOptions options)
9 | : base(options)
10 | {
11 | Tokens = Set();
12 | }
13 |
14 | public DbSet Tokens { get; }
15 |
16 | public async ValueTask AddToken(string jwt, DateTimeOffset expires)
17 | {
18 | await Tokens.AddAsync(new GeneratedToken
19 | {
20 | Token = jwt,
21 | Expires = expires
22 | });
23 | await SaveChangesAsync();
24 | }
25 |
26 | public async ValueTask RemoveToken(string jwt)
27 | {
28 | var tokens = await Tokens.Where(x => x.Token == jwt).ToArrayAsync();
29 | if(tokens.Any())
30 | {
31 | Tokens.RemoveRange(tokens);
32 | await SaveChangesAsync();
33 | }
34 | }
35 |
36 | public async ValueTask TokenExists(string jwt) =>
37 | await Tokens.AnyAsync(x => x.Token == jwt && x.Expires > DateTimeOffset.Now);
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Mobile Auth CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | paths:
7 | - "*.props"
8 | - "sample/**"
9 | - "src/**"
10 | - "version.json"
11 | - ".github/workflows/ci.yml"
12 | workflow_dispatch:
13 |
14 | jobs:
15 | build:
16 | uses: avantipoint/workflow-templates/.github/workflows/dotnet-build.yml@master
17 | permissions:
18 | statuses: write
19 | checks: write
20 | with:
21 | name: Mobile Auth
22 | solution-path: build.slnf
23 | run-tests: false
24 | code-sign: true
25 | secrets:
26 | codeSignKeyVault: ${{ secrets.CodeSignKeyVault }}
27 | codeSignClientId: ${{ secrets.CodeSignClientId }}
28 | codeSignTenantId: ${{ secrets.CodeSignTenantId }}
29 | codeSignClientSecret: ${{ secrets.CodeSignClientSecret }}
30 | codeSignCertificate: ${{ secrets.CodeSignCertificate }}
31 |
32 | deploy-internal:
33 | uses: avantipoint/workflow-templates/.github/workflows/deploy-nuget.yml@master
34 | needs: build
35 | with:
36 | name: Deploy Internal
37 | secrets:
38 | feedUrl: ${{ secrets.IN_HOUSE_NUGET_FEED }}
39 | apiKey: ${{ secrets.IN_HOUSE_API_KEY }}
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Configuration/OAuthLibraryOptions.cs:
--------------------------------------------------------------------------------
1 | using AvantiPoint.MobileAuth.Authentication;
2 |
3 | namespace AvantiPoint.MobileAuth.Configuration;
4 |
5 | internal class OAuthLibraryOptions : ITokenOptions
6 | {
7 | public string? AuthPath { get; set; }
8 |
9 | public string? CallbackScheme { get; set; }
10 |
11 | public string? JwtKey { get; set; }
12 |
13 | public TimeSpan DefaultExpiration { get; set; } = TimeSpan.FromMinutes(30);
14 |
15 | public bool OverrideTokenExpiration { get; set; }
16 |
17 | public AppleOAuthOptions? Apple { get; set; }
18 |
19 | public GoogleProviderOptions? Google { get; set; }
20 |
21 | public MicrosoftProviderOptions? Microsoft { get; set; }
22 |
23 | internal string Signin => $"/{SanitizePath()}/signin";
24 |
25 | internal string Signout => $"/{SanitizePath()}/signout";
26 |
27 | internal string Refresh => $"/{SanitizePath()}/refresh";
28 |
29 | private string SanitizePath()
30 | {
31 | var path = AuthPath;
32 | if (!string.IsNullOrEmpty(path))
33 | {
34 | if (path.EndsWith('/'))
35 | path = path.Substring(0, path.Length - 1);
36 |
37 | if (path.StartsWith('/'))
38 | path = path.Substring(1);
39 | }
40 |
41 | if (string.IsNullOrEmpty(path))
42 | path = "mobileauth";
43 |
44 | return path;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/MacCatalyst/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIDeviceFamily
6 |
7 | 1
8 | 2
9 |
10 | UIRequiredDeviceCapabilities
11 |
12 | arm64
13 |
14 | UISupportedInterfaceOrientations
15 |
16 | UIInterfaceOrientationPortrait
17 | UIInterfaceOrientationLandscapeLeft
18 | UIInterfaceOrientationLandscapeRight
19 |
20 | UISupportedInterfaceOrientations~ipad
21 |
22 | UIInterfaceOrientationPortrait
23 | UIInterfaceOrientationPortraitUpsideDown
24 | UIInterfaceOrientationLandscapeLeft
25 | UIInterfaceOrientationLandscapeRight
26 |
27 | XSAppIconAssets
28 | Assets.xcassets/appicon.appiconset
29 | CFBundleURLTypes
30 |
31 |
32 | CFBundleURLName
33 | DemoMobileApp
34 | CFBundleURLSchemes
35 |
36 | myapp-scheme
37 |
38 | CFBundleTypeRole
39 | Editor
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSRequiresIPhoneOS
6 |
7 | UIDeviceFamily
8 |
9 | 1
10 | 2
11 |
12 | UIRequiredDeviceCapabilities
13 |
14 | arm64
15 |
16 | UISupportedInterfaceOrientations
17 |
18 | UIInterfaceOrientationPortrait
19 | UIInterfaceOrientationLandscapeLeft
20 | UIInterfaceOrientationLandscapeRight
21 |
22 | UISupportedInterfaceOrientations~ipad
23 |
24 | UIInterfaceOrientationPortrait
25 | UIInterfaceOrientationPortraitUpsideDown
26 | UIInterfaceOrientationLandscapeLeft
27 | UIInterfaceOrientationLandscapeRight
28 |
29 | XSAppIconAssets
30 | Assets.xcassets/appicon.appiconset
31 | CFBundleURLTypes
32 |
33 |
34 | CFBundleURLName
35 | DemoMobileApp
36 | CFBundleURLSchemes
37 |
38 | myapp-scheme
39 |
40 | CFBundleTypeRole
41 | Editor
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/sample/DemoAPI/CustomClaimsHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using AvantiPoint.MobileAuth;
3 | using DemoAPI.Data;
4 | using Microsoft.AspNetCore.Authentication;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | // If needed provide this as a Scoped Service.
8 | public class CustomClaimsHandler : MobileAuthClaimsHandler
9 | {
10 | private UserContext _userContext { get; }
11 | public CustomClaimsHandler(UserContext userContext)
12 | {
13 | _userContext = userContext;
14 | }
15 |
16 | public override async ValueTask> GenerateClaims(HttpContext context, AuthenticateResult auth, string scheme)
17 | {
18 | var claims = (await base.GenerateClaims(context, auth, scheme).ConfigureAwait(false)).ToList();
19 | var email = FindFirstValue(claims, "email");
20 | if (string.IsNullOrEmpty(email))
21 | throw new InvalidOperationException("The claims do not contain an email claim");
22 |
23 | // Need to update a database or specify specific claims? You can do that here...
24 | var userRoles = await _userContext.UserRoles.Where(x => x.Email == email).ToArrayAsync().ConfigureAwait(false);
25 | if(!userRoles.Any())
26 | {
27 | userRoles = new UserRole[] { new UserRole { Email = email, Role = "GenericUser" } };
28 | await _userContext.UserRoles.AddRangeAsync(userRoles).ConfigureAwait(false);
29 | await _userContext.SaveChangesAsync().ConfigureAwait(false);
30 | }
31 |
32 | claims.AddRange(userRoles.Select(x => new Claim(ClaimTypes.Role, x.Role)));
33 |
34 | return claims;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Authentication/MobileJwtValidationHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Encodings.Web;
2 | using System.Text.RegularExpressions;
3 | using Microsoft.AspNetCore.Authentication;
4 | using Microsoft.AspNetCore.Authentication.JwtBearer;
5 | using Microsoft.Extensions.Logging;
6 | using Microsoft.Extensions.Options;
7 |
8 | namespace AvantiPoint.MobileAuth.Authentication;
9 |
10 | internal sealed class MobileJwtValidationHandler : JwtBearerHandler
11 | {
12 | private ITokenService _tokenService { get; }
13 |
14 | public MobileJwtValidationHandler(
15 | IOptionsMonitor options,
16 | ILoggerFactory logger,
17 | UrlEncoder encoder,
18 | ITokenService tokenService,
19 | ISystemClock clock)
20 | : base(options, logger, encoder, clock)
21 | {
22 | _tokenService = tokenService;
23 | }
24 |
25 | protected override async Task HandleAuthenticateAsync()
26 | {
27 | var host = $"{Request.Scheme}://{Request.Host.Value}";
28 | Options.TokenValidationParameters.ValidAudience = host;
29 | Options.TokenValidationParameters.ValidIssuer = host;
30 | Options.TokenValidationParameters.IssuerSigningKey = _tokenService.GetKey();
31 |
32 | string? header = Request.Headers.Authorization;
33 | if (string.IsNullOrEmpty(header))
34 | return AuthenticateResult.NoResult();
35 |
36 | var token = Regex.Replace(header, "Bearer", string.Empty).Trim();
37 | if (string.IsNullOrEmpty(token))
38 | return AuthenticateResult.NoResult();
39 | else if (!await _tokenService.IsTokenValid(token))
40 | return AuthenticateResult.Fail("No valid token found");
41 |
42 | return await base.HandleAuthenticateAsync();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/Splash/splash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/AppIcon/appiconfg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/MauiProgram.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Maui;
2 | using DemoMobileApp.Services;
3 | using DemoMobileApp.ViewModels;
4 | using Refit;
5 |
6 | namespace DemoMobileApp;
7 |
8 | public static class Constants
9 | {
10 | public const string BaseUrl = "https://localhost:7172";
11 | public const string CallbackScheme = "myapp-scheme";
12 | }
13 |
14 | public static class MauiProgram
15 | {
16 | public static MauiApp CreateMauiApp()
17 | {
18 | var builder = MauiApp.CreateBuilder();
19 | builder
20 | .UseMauiMicroMvvm(
21 | "Resources/Styles/Colors.xaml",
22 | "Resources/Styles/Styles.xaml")
23 | .UseMauiCommunityToolkit()
24 | .ConfigureFonts(fonts =>
25 | {
26 | fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
27 | fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
28 | });
29 |
30 | builder.Services.AddSingleton(WebAuthenticator.Default)
31 | .AddSingleton(AppleSignInAuthenticator.Default)
32 | .AddSingleton(SecureStorage.Default)
33 | .AddRefitClient()
34 | .AddTransient()
35 | .AddTransient();
36 |
37 | return builder.Build();
38 | }
39 |
40 | public static IServiceCollection AddRefitClient(this IServiceCollection services)
41 | where T : class
42 | {
43 | services.AddSingleton(sp =>
44 | {
45 | var settings = new RefitSettings
46 | {
47 | AuthorizationHeaderValueGetter = () => sp.GetRequiredService().GetAsync("access_token")
48 | };
49 | return RestService.For(Constants.BaseUrl, settings);
50 | });
51 | return services;
52 | }
53 | }
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Platforms/Windows/Package.appxmanifest:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 | $placeholder$
12 | User Name
13 | $placeholder$.png
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | DemoMobileApp
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/ViewModels/MainPageViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.ObjectModel;
2 | using DemoMobileApp.Services;
3 | using MauiMicroMvvm;
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace DemoMobileApp.ViewModels;
7 |
8 | public class MainPageViewModel : MauiMicroViewModel
9 | {
10 | private ISecureStorage _storage { get; }
11 | private IWebAuthenticator _webAuthenticator { get; }
12 | private IUserProfileService _userProfile { get; }
13 |
14 | public MainPageViewModel(ViewModelContext context, ISecureStorage storage, IWebAuthenticator webAuthenticator, IUserProfileService userProfile)
15 | : base(context)
16 | {
17 | _storage = storage;
18 | _webAuthenticator = webAuthenticator;
19 | _userProfile = userProfile;
20 | LoginCommand = new (OnLoginCommandExecuted);
21 | Claims = new();
22 | }
23 |
24 | public ObservableCollection Claims { get; }
25 |
26 | public string Email
27 | {
28 | get => Get();
29 | set => Set(value);
30 | }
31 |
32 | public Command LoginCommand { get; }
33 |
34 | private async void OnLoginCommandExecuted(string scheme)
35 | {
36 | try
37 | {
38 | var result = await _webAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions
39 | {
40 | CallbackUrl = new Uri($"{Constants.CallbackScheme}://"),
41 | Url = new Uri(new Uri(Constants.BaseUrl), $"mobileauth/{scheme}")
42 | });
43 |
44 | await _storage.SetAsync("access_token", result.AccessToken);
45 |
46 | using var response = await _userProfile.GetProfileClaims();
47 | if(response.IsSuccessStatusCode)
48 | {
49 | Claims.Clear();
50 | var claims = response.Content.Select(x => $"{x.Key}: {x.Value}");
51 | foreach (var claim in claims)
52 | Claims.Add(claim);
53 |
54 | if (response.Content.TryGetValue("email", out var email))
55 | Email = email;
56 | }
57 | }
58 | catch (Exception ex)
59 | {
60 | Logger.LogError(ex, "Unexpected error occurred.");
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/sample/DemoAPI/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using AvantiPoint.MobileAuth;
3 | using DemoAPI.Data;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.OpenApi.Models;
6 |
7 | var builder = WebApplication.CreateBuilder(args);
8 |
9 | builder.AddMobileAuth(auth =>
10 | {
11 | // Configure override for Token Store
12 | auth.ConfigureDbTokenStore(o => o.UseInMemoryDatabase("DemoApi"));
13 | // Configure override for Claims Handler
14 | auth.AddMobileAuthClaimsHandler();
15 |
16 | // Add Additional Providers like Facebook, Twitter, LinkedIn, GitHub, etc...
17 | });
18 |
19 | // Add services to the container.
20 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
21 | builder.Services.AddEndpointsApiExplorer();
22 | var versionInfo = FileVersionInfo.GetVersionInfo(typeof(MobileAuth).Assembly.Location);
23 | var version = $"{versionInfo.FileMajorPart}.{versionInfo.ProductMinorPart}.{versionInfo.ProductBuildPart}";
24 | builder.Services.AddSwaggerGen(c =>
25 | {
26 | c.SwaggerDoc($"v1", new OpenApiInfo
27 | {
28 | Title = "Mobile Auth - Demo",
29 | Contact = new OpenApiContact
30 | {
31 | Name = "AvantiPoint",
32 | Email = "hello@avantipoint.com",
33 | Url = new Uri("https://avantipoint.com")
34 | },
35 | Description = "This is a demo api for the AvantiPoint Mobile Auth library. Do not use this API for production. For more information please visit https://github.com/avantipoint/mobileauth-lib",
36 | Version = version
37 | });
38 | });
39 |
40 | var app = builder.Build();
41 |
42 | // Configure the HTTP request pipeline.
43 |
44 | app.UseHttpsRedirection();
45 | app.UseSwagger();
46 | app.UseSwaggerUI(o =>
47 | {
48 | o.InjectStylesheet("https://cdn.avantipoint.com/theme/swagger/style.css");
49 | });
50 |
51 | app.UseAuthentication();
52 | app.UseAuthorization();
53 |
54 | app.Map("/", async context =>
55 | {
56 | await Task.CompletedTask;
57 | context.Response.Redirect("/swagger");
58 | });
59 |
60 | // maps https://{host}/mobileauth/{Apple|Google|Microsoft}
61 | app.MapDefaultMobileAuthRoutes();
62 | //app.MapMobileAuthRoute();
63 | //app.MapMobileAuthLogoutRoute();
64 | //app.MapMobileAuthUserClaimsRoute("/profile");
65 |
66 | app.Run();
67 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | en
4 | True
5 | True
6 | Dan Siegel
7 | AvantiPoint
8 | Copyright © AvantiPoint 2016-$([System.DateTime]::Now.Year)
9 | LICENSE
10 | avantipoint-icon.png
11 | https://github.com/AvantiPoint/mobileauth-lib
12 | git
13 | https://github.com/AvantiPoint/mobileauth-lib.git
14 | $(MSBuildThisFileDirectory)Artifacts
15 | $(BUILD_ARTIFACTSTAGINGDIRECTORY)
16 | true
17 | false
18 | $(IsPackable)
19 | $(CI)
20 |
21 |
22 |
23 |
24 | snupkg
25 | true
26 | true
27 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
28 |
29 |
30 |
31 |
35 |
39 |
40 |
41 |
42 |
43 |
46 |
49 |
50 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/Styles/Colors.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | #512BD4
8 | #DFD8F7
9 | #2B0B98
10 | White
11 | Black
12 | #E1E1E1
13 | #C8C8C8
14 | #ACACAC
15 | #919191
16 | #6E6E6E
17 | #404040
18 | #212121
19 | #141414
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | #F7B548
35 | #FFD590
36 | #FFE5B9
37 | #28C2D1
38 | #7BDDEF
39 | #C3F2F4
40 | #3E8EED
41 | #72ACF1
42 | #A7CBF6
43 |
44 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/AvantiPoint.MobileAuth.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;net7.0
5 | enable
6 | enable
7 | true
8 | The MobileAuth Library is designed to quickly stand up an OAuth endpoint for Apple, Google, & Microsoft providers with the flexibility of customizing additional ones or only providing ones that you provide a configuration for. This makes it easy to stand up a minimal API with only a few lines of code.
9 | apple;google;microsoft;oauth;minimal api
10 | False
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/MainPage.xaml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
24 |
27 |
30 |
34 |
35 |
36 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/MobileAuthenticationBuilder.cs:
--------------------------------------------------------------------------------
1 | using AvantiPoint.MobileAuth.Authentication;
2 | using AvantiPoint.MobileAuth.Stores;
3 | using FileContextCore;
4 | using FileContextCore.FileManager;
5 | using FileContextCore.Serializer;
6 | using Microsoft.AspNetCore.Authentication;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using Microsoft.Extensions.DependencyInjection.Extensions;
10 | using System.Reflection;
11 |
12 | namespace AvantiPoint.MobileAuth;
13 |
14 | public class MobileAuthenticationBuilder : AuthenticationBuilder
15 | {
16 | public MobileAuthenticationBuilder(AuthenticationBuilder builder)
17 | : base(builder.Services)
18 | {
19 | }
20 |
21 | public MobileAuthenticationBuilder AddMobileAuthClaimsHandler()
22 | where T : class, IMobileAuthClaimsHandler
23 | {
24 | Services.AddScoped();
25 | return this;
26 | }
27 |
28 | public MobileAuthenticationBuilder AddTokenService()
29 | where T : class, ITokenService
30 | {
31 | Services.AddScoped();
32 | return this;
33 | }
34 |
35 | private bool tokenStoreConfigured;
36 | public MobileAuthenticationBuilder ConfigureTokenStore()
37 | where TStore : class, ITokenStore
38 | {
39 | tokenStoreConfigured = true;
40 | Services.AddScoped();
41 | return this;
42 | }
43 |
44 | public MobileAuthenticationBuilder ConfigureDbTokenStore(Action? optionsAction = null)
45 | where TStore : DbContext, ITokenStore
46 | {
47 | tokenStoreConfigured = true;
48 | Services.AddDbContext(optionsAction)
49 | .AddScoped(sp => sp.GetRequiredService());
50 | return this;
51 | }
52 |
53 | public MobileAuthenticationBuilder ConfigureDbTokenStore(Action optionsAction)
54 | where TStore : DbContext, ITokenStore
55 | {
56 | tokenStoreConfigured = true;
57 | Services.AddDbContext(optionsAction)
58 | .AddScoped(sp => sp.GetRequiredService());
59 | return this;
60 | }
61 |
62 | internal void ConfigureDefaultServices()
63 | {
64 | Services.TryAddScoped();
65 | Services.TryAddScoped();
66 | if (!tokenStoreConfigured)
67 | ConfigureDbTokenStore((services, options) =>
68 | {
69 | options.UseFileContextDatabase(
70 | databaseName: "mobileauth",
71 | location: Path.Join("App_Data", "auth"));
72 | });
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/MobileAuthClaimsHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using Microsoft.AspNetCore.Authentication;
3 | using Microsoft.AspNetCore.Http;
4 |
5 | namespace AvantiPoint.MobileAuth;
6 |
7 | public class MobileAuthClaimsHandler : IMobileAuthClaimsHandler
8 | {
9 | public virtual ValueTask> GenerateClaims(HttpContext context, AuthenticateResult auth, string scheme)
10 | {
11 | if (auth.Principal is null)
12 | throw new NullReferenceException("The Authentication Result Principal is null.");
13 |
14 | var claims = GetClaims(auth.Principal);
15 |
16 | claims["provider"] = scheme;
17 |
18 | if(auth.Properties is not null)
19 | {
20 | claims["access_token"] = auth.Properties.GetTokenValue("access_token") ?? string.Empty;
21 | claims["id_token"] = auth.Properties.GetTokenValue("id_token") ?? string.Empty;
22 | claims["refresh_token"] = auth.Properties.GetTokenValue("refresh_token") ?? string.Empty;
23 | claims["expires_in"] = (auth.Properties.ExpiresUtc?.ToUnixTimeSeconds() ?? -1).ToString();
24 | }
25 |
26 | ConfigureName(ref claims);
27 |
28 | return ValueTask.FromResult(claims.Where(x => !string.IsNullOrEmpty(x.Value) && x.Value != "-1")
29 | .Select(x => new Claim(x.Key, x.Value)));
30 | }
31 |
32 | private static void ConfigureName(ref Dictionary claims)
33 | {
34 | if (claims.TryGetValue("name", out var name) && !string.IsNullOrEmpty(name))
35 | return;
36 | else if (claims.TryGetValue("surname", out var surname) && claims.TryGetValue("given_name", out var givenname) && !string.IsNullOrEmpty(surname) && !string.IsNullOrEmpty(givenname))
37 | claims["name"] = $"{givenname} {surname}".Trim();
38 | }
39 |
40 | private static Dictionary GetClaims(ClaimsPrincipal principal)
41 | {
42 | var claims = new Dictionary();
43 |
44 | AddClaim(ref claims, "email", principal.FindFirstValue(ClaimTypes.Email));
45 | AddClaim(ref claims, "name", principal.FindFirstValue(ClaimTypes.Name));
46 | AddClaim(ref claims, "given_name", principal.FindFirstValue(ClaimTypes.GivenName));
47 | AddClaim(ref claims, "surname", principal.FindFirstValue(ClaimTypes.Surname));
48 | AddClaim(ref claims, "provider_id", principal.FindFirstValue(ClaimTypes.NameIdentifier));
49 | return claims;
50 | }
51 |
52 | private static void AddClaim(ref Dictionary claims, string claim, string? value)
53 | {
54 | if (!claims.ContainsKey(claim) && !string.IsNullOrEmpty(value))
55 | claims[claim] = value;
56 | }
57 |
58 | protected static string? FindFirstValue(IEnumerable claims, string type) =>
59 | claims.FirstOrDefault(x => x.Type== type)?.Value;
60 | }
61 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Configuration/AppleOAuthOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 | using AspNet.Security.OAuth.Apple;
3 | using Azure.Security.KeyVault.Secrets;
4 | using Microsoft.AspNetCore.Authentication;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 |
9 | namespace AvantiPoint.MobileAuth.Configuration;
10 |
11 | internal class AppleOAuthOptions
12 | {
13 | public string? ServiceId { get; set; }
14 |
15 | public string? KeyId { get; set; }
16 |
17 | public string? TeamId { get; set; }
18 |
19 | public string? PrivateKey { get; set; }
20 |
21 | public bool UseAzureKeyVault { get; set; }
22 |
23 | public void Configure(AuthenticationBuilder builder, WebApplicationBuilder appBuilder)
24 | {
25 | if (string.IsNullOrEmpty(ServiceId) || string.IsNullOrEmpty(KeyId) || string.IsNullOrEmpty(TeamId))
26 | return;
27 |
28 | else if (UseAzureKeyVault)
29 | builder.AddApple()
30 | .Services
31 | .AddOptions(AppleAuthenticationDefaults.AuthenticationScheme)
32 | .Configure((o, configuration, client) =>
33 | {
34 | o.Scope.Add("name");
35 | o.Scope.Add("email");
36 | o.ClientId = ServiceId;
37 | o.KeyId = KeyId;
38 | o.TeamId = TeamId;
39 | o.PrivateKey = async (keyId, cancellationToken) =>
40 | {
41 | var secret = await client.GetSecretAsync($"AuthKey_{keyId}", cancellationToken: cancellationToken);
42 | return secret.Value.Value.AsMemory();
43 | };
44 | });
45 |
46 | else
47 | builder.AddApple(o =>
48 | {
49 | o.Scope.Add("name");
50 | o.Scope.Add("email");
51 | o.ClientId = ServiceId;
52 | o.KeyId = KeyId;
53 | o.TeamId = TeamId;
54 | if (!string.IsNullOrEmpty(PrivateKey))
55 | {
56 | var path = Path.Combine(appBuilder.Environment.ContentRootPath, "App_Data", $"Apple-AuthKey.p8");
57 | new FileInfo(path).Directory?.Create();
58 | File.WriteAllText(path, PrivateKey);
59 | o.UsePrivateKey(keyId =>
60 | appBuilder.Environment.ContentRootFileProvider.GetFileInfo(Path.Combine("App_Data", "Apple-AuthKey.p8")));
61 | }
62 | else
63 | o.UsePrivateKey(keyId =>
64 | appBuilder.Environment.ContentRootFileProvider.GetFileInfo(Path.Combine("App_Data", $"AuthKey_{keyId}.p8")));
65 |
66 | o.SaveTokens = true;
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/AvantiPoint.MobileAuth.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32519.111
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvantiPoint.MobileAuth", "src\AvantiPoint.MobileAuth\AvantiPoint.MobileAuth.csproj", "{E2019D64-0A21-4502-B615-D752387BA232}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoAPI", "sample\DemoAPI\DemoAPI.csproj", "{161986B4-EC99-4971-9560-6CE255D1472C}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{64098382-030D-474A-977A-A6A5A3DE4352}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{C2F3B11F-60AC-4BE1-B01D-A15BCEA54B64}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoMobileApp", "sample\DemoMobileApp\DemoMobileApp.csproj", "{1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {E2019D64-0A21-4502-B615-D752387BA232}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {E2019D64-0A21-4502-B615-D752387BA232}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {E2019D64-0A21-4502-B615-D752387BA232}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {E2019D64-0A21-4502-B615-D752387BA232}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {161986B4-EC99-4971-9560-6CE255D1472C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {161986B4-EC99-4971-9560-6CE255D1472C}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {161986B4-EC99-4971-9560-6CE255D1472C}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {161986B4-EC99-4971-9560-6CE255D1472C}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
33 | {1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6}.Release|Any CPU.Build.0 = Release|Any CPU
35 | {1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6}.Release|Any CPU.Deploy.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(NestedProjects) = preSolution
41 | {E2019D64-0A21-4502-B615-D752387BA232} = {64098382-030D-474A-977A-A6A5A3DE4352}
42 | {161986B4-EC99-4971-9560-6CE255D1472C} = {C2F3B11F-60AC-4BE1-B01D-A15BCEA54B64}
43 | {1D8FD7CD-DFA6-451C-B41C-B4796D0D69C6} = {C2F3B11F-60AC-4BE1-B01D-A15BCEA54B64}
44 | EndGlobalSection
45 | GlobalSection(ExtensibilityGlobals) = postSolution
46 | SolutionGuid = {6C9E9E68-E9EB-41A8-B635-638B7DE62801}
47 | EndGlobalSection
48 | EndGlobal
49 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/DemoMobileApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0-android;net7.0-ios;net7.0-maccatalyst
5 | $(TargetFrameworks);net7.0-windows10.0.19041.0
6 |
7 |
8 | Exe
9 | DemoMobileApp
10 | true
11 | true
12 | enable
13 |
14 |
15 | DemoMobileApp
16 |
17 |
18 | com.companyname.demomobileapp
19 | 735CEA41-7605-401D-BD30-82403D90AA01
20 |
21 |
22 | 1.0
23 | 1
24 |
25 | 14.2
26 | 14.0
27 | 21.0
28 | 10.0.17763.0
29 | 10.0.17763.0
30 | 6.5
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/Authentication/TokenService.cs:
--------------------------------------------------------------------------------
1 | using System.IdentityModel.Tokens.Jwt;
2 | using System.Security.Claims;
3 | using System.Text;
4 | using AvantiPoint.MobileAuth.Stores;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.Extensions.Logging;
7 | using Microsoft.IdentityModel.Tokens;
8 |
9 | namespace AvantiPoint.MobileAuth.Authentication;
10 |
11 | public class TokenService : ITokenService
12 | {
13 | private ITokenOptions _options { get; }
14 |
15 | private ITokenStore _tokenStore { get; }
16 |
17 | private IHttpContextAccessor _contextAccessor { get; }
18 |
19 | private ILogger _logger { get; }
20 |
21 | public TokenService(IHttpContextAccessor contextAccessor, ILogger logger, ITokenStore tokenStore, ITokenOptions options)
22 | {
23 | _contextAccessor = contextAccessor;
24 | _logger = logger;
25 | _options = options;
26 | _tokenStore = tokenStore;
27 | }
28 |
29 | public async ValueTask BuildToken(IEnumerable userClaims)
30 | {
31 | var defaultExpiration = _options.DefaultExpiration == default ? TimeSpan.FromMinutes(30) : _options.DefaultExpiration;
32 |
33 | var expires = DateTimeOffset.UtcNow.Add(defaultExpiration);
34 | if (userClaims.ContainsKey("expires_in") &&
35 | long.TryParse(userClaims.FindFirstValue("expires_in"), out var expires_in) &&
36 | expires_in > 0)
37 | {
38 | if(_options.OverrideTokenExpiration)
39 | expires = DateTimeOffset.FromUnixTimeSeconds(expires_in);
40 | }
41 | else if(_options.OverrideTokenExpiration)
42 | {
43 | _logger.LogInformation("Unable to override token expiration. The provided OAuth token does not have a valid `expires_in` claim.");
44 | }
45 |
46 | var claims = userClaims.Where(x => !string.IsNullOrEmpty(x.Value) && x.Value != "-1");
47 |
48 | var host = GetHost();
49 | _logger.LogInformation($"Using '{host}' for the token host.");
50 | var securityKey = GetKey();
51 | var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
52 | var tokenDescriptor = new JwtSecurityToken(host, host, claims,
53 | expires: expires.UtcDateTime, signingCredentials: credentials);
54 | var jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
55 | await _tokenStore.AddToken(jwt, expires);
56 | await OnTokenCreated(tokenDescriptor, jwt);
57 | return jwt;
58 | }
59 |
60 | protected virtual ValueTask OnTokenCreated(JwtSecurityToken securityToken, string jwt) => ValueTask.CompletedTask;
61 |
62 | public virtual async ValueTask IsTokenValid(string token)
63 | {
64 | if (string.IsNullOrEmpty(token))
65 | return false;
66 |
67 | try
68 | {
69 | if (!await _tokenStore.TokenExists(token))
70 | return false;
71 |
72 | var host = GetHost();
73 | var tokenHandler = new JwtSecurityTokenHandler();
74 | tokenHandler.ValidateToken(token, new TokenValidationParameters
75 | {
76 | ValidateIssuerSigningKey = true,
77 | ValidateIssuer = true,
78 | ValidateAudience = true,
79 | ValidIssuer = host,
80 | ValidAudience = host,
81 | IssuerSigningKey = GetKey(),
82 | }, out SecurityToken validatedToken);
83 |
84 | var now = DateTime.UtcNow;
85 | return now >= validatedToken.ValidFrom && now <= validatedToken.ValidTo;
86 | }
87 | catch
88 | {
89 | return false;
90 | }
91 | }
92 |
93 | public async ValueTask InvalidateToken(string token)
94 | {
95 | _logger.LogInformation("Invalidating Token.");
96 | await _tokenStore.RemoveToken(token);
97 | _logger.LogInformation("Token Invalidated.");
98 | await OnTokenInvalidated(token);
99 | }
100 |
101 | protected virtual ValueTask OnTokenInvalidated(string token) => ValueTask.CompletedTask;
102 |
103 | public SymmetricSecurityKey GetKey()
104 | {
105 | var key = _options.JwtKey;
106 | if (string.IsNullOrEmpty(key))
107 | {
108 | _logger.LogWarning("No key has been configured. Using default development key. Please provide a configuration value for 'OAuth:JwtKey'.");
109 | key = GetType().AssemblyQualifiedName;
110 | }
111 |
112 | return GetKey(key);
113 | }
114 |
115 | private string GetHost()
116 | {
117 | var request = _contextAccessor.HttpContext?.Request;
118 | if (request is null)
119 | throw new ArgumentNullException("HttpContext");
120 |
121 | return $"{request.Scheme}://{request.Host.Value}";
122 | }
123 |
124 | internal static SymmetricSecurityKey GetKey(string? key)
125 | {
126 | if (string.IsNullOrEmpty(key))
127 | throw new ArgumentNullException(nameof(key));
128 |
129 | var data = Encoding.UTF8.GetBytes(key);
130 | return new SymmetricSecurityKey(data);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/ReadMe.md:
--------------------------------------------------------------------------------
1 | # MobileAuth Library
2 |
3 | The MobileAuth Library is a helper library to help produce an OAuth endpoint using AspNetCore Minimal APIs for your Mobile Application. This can be done in just a few lines of code. Out of the box using the library you can support Sign In with Apple, Google, and Microsoft Accounts. These require no manual configuration in code and only for the configuration values to be added to the host or `appsettings.json` file. Additional / Custom providers can easily be added as well.
4 |
5 | ```cs
6 | var builder = WebApplication.CreateBuilder(args);
7 |
8 | builder.AddMobileAuth();
9 |
10 | var app = builder.Build();
11 |
12 | app.UseHttpsRedirection();
13 | app.UseAuthentication();
14 | app.UseAuthorization();
15 |
16 | // maps https://{host}/mobileauth/{Apple|Google|Microsoft}
17 | app.MapMobileAuthRoute();
18 |
19 | app.Run();
20 | ```
21 |
22 | ## Configuration
23 |
24 | The only required part of the configuration is the CallbackScheme. This can be anything you want and will be used in the redirect url. Note the redirect url will be formatted as `{CallbackScheme}://auth?access_token={jwt}&expires_in={expires timestamp in Unix Seconds}`. This is meant to be used with the Xamarin or Maui Essentials WebAuthenticator.
25 |
26 | ```json
27 | {
28 | "OAuth": {
29 | "CallbackScheme": "yourappscheme",
30 | "JwtKey": "yoursecretkey",
31 | "Apple": {
32 | "ServiceId": "{Apple Service Id}",
33 | "TeamId": "{Your Apple Team Id}",
34 | "KeyId": "{Your Apple Key Id}",
35 | },
36 | "Google": {
37 | "ClientId": "{Google Client Id}",
38 | "ClientSecret": "{Your Google Client Secret}",
39 | },
40 | "Microsoft": {
41 | "ClientId": "{Microsoft Client Id}",
42 | "ClientSecret": "{Your Microsoft Client Secret}",
43 | }
44 | }
45 | }
46 | ```
47 |
48 | ### Jwt
49 |
50 | In order to better assist you in providing authentication with your API the library will automatically wrap any claims into a self signed JWT. By default if no key is provided it will use a development key. You should be sure to update this for production scenarios. After your user has been authenticated you can use the AccessToken to authenticate with your API. Within this JWT you may find an original access token from the OAuth provider. If you need to access any API from Google or Microsoft for example you can use the original access token to authenticate with the API.
51 |
52 | ### Apple Configuration
53 |
54 | As with any app you will need to set up a new App Id in the Apple Developer Portal. Before you get very far you can grab the Team Id out of the Developer Portal. Just beneath your name in the Developer Portal you should see the Company Name / Team Name along with the Team Id `My Company - VK8ZR2JK2E`. You'll use the `VK8ZR2JK2E` as the Team Id in your configuration.
55 |
56 | If you have not already created an App Id, you should start there. For this example we'll say the App Id is `com.example.myapp`. Be sure to enable the `Sign In with Apple` capability.
57 |
58 | Once you've done this you should create a Key. Select the Keys option and then create a new Key. You can give it a name like `MyAppSIWA`, be sure to select the `Sign in with Apple` option. You'll need to click the configure button and select the Primary App Id that you created in the previous step, and hit save.
59 |
60 | > NOTE:
61 | > When selecting the primary app id, it will show up like `My Awesome App (DKD783KDELD.com.example.myapp)`, where `DKD783KDELD` is the App Id. It will then show below a `Grouped App Id` like `DKD783KDELD.com.example.myapp.sid`.
62 |
63 | Once you have the Key, it should have downloaded with a file name like `AuthKey_IUK783KD3R9.p8`, where `IUK783KD3R9` is the Key Id that you will need for your configuration.
64 |
65 | When you're done you'll want to go back to the Identifiers and toggle from `App IDs` to `Service IDs`. You will need to create a the Service Id for your App as `com.example.myapp.sid` which you saw in the Grouped App Id, you will naturally provide this as the Service Id in your configuration. Again enable the `Sign In with Apple` capability, and this time when you configure it, it will prompt you for a host name and callback. Apple will NOT allow you to use localhost as an authorized host. You must deploy this or update your hosts file have something like `myapp.com` mapped back to `127.0.0.1`. You can then use `myapp.com` as an authorized host where the callback is `https://myapp.com/signin-apple`.
66 |
67 | > NOTE:
68 | > Be sure the generated key is in the `App_Data` directory with the name `AuthKey_{Your KeyId}.p8`.
69 |
70 | To provide additional flexibility you can provide values for the following optional configuration values:
71 |
72 | ```json
73 | {
74 | "OAuth": {
75 | "Apple": {
76 | "PrivateKey": "{The text value for your private key}", // Recommended for development only
77 | "UseAzureKeyVault": true // Optional, defaults to false
78 | }
79 | }
80 | }
81 | ```
82 |
83 | When using Azure Key Vault we will only update the Apple Registration to ensure that your p8 is loaded from the Azure Key Vault however you will still need to properly configure your application to [connect to the Azure Key Vault](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0&WT.mc_id=DT-MVP-5002924).
84 |
85 | ### Google / Microsoft Configuration
86 |
87 | Microsoft actually has decent docs on this please see:
88 |
89 | - [Google](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-6.0&WT.mc_id=DT-MVP-5002924) - To get your client id and secret go to [Google API & Services](https://console.cloud.google.com/apis/credentials)
90 | - [Microsoft](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/microsoft-logins?view=aspnetcore-6.0&WT.mc_id=DT-MVP-5002924) - To get your client id & secret [create an Application in Azure](https://go.microsoft.com/fwlink/?linkid=2083908&WT.mc_id=DT-MVP-5002924)
91 |
92 | Again once you've got your Client Id & Client Secret you simply need to provide them in your configuration when using this library.
93 |
94 | ### Additional Providers
95 |
96 | You can opt out of using any built in providers by simply not providing the required configuration values. In order to add additional providers you can access the AuthenticationBuilder and register any other providers you may need when calling the `AddMobileAuth` method.
97 |
98 | ```cs
99 | builder.AddMobileAuth(auth => {
100 | auth.AddFacebook(o => {
101 | o.ClientId = "{Facebook Client Id}";
102 | o.ClientSecret = "{Facebook Client Secret}";
103 | });
104 | // etc...
105 | });
106 | ```
107 |
108 | ### Customize Returned Claims
109 |
110 | By Default the library will attempt to return the following claims:
111 |
112 | - The User's Given Name, Surname, & Full Name
113 | - The User's Email Address
114 | - The Authentication Provider (Apple, Google, Microsoft)
115 | - The Authentication Provider's User/Object Id
116 | - The Access & Refresh Tokens
117 | - When the Token Expires as a UTC time in Unix Seconds
118 |
119 | Whether you need to inject some additional logic or if you just want to customize how the claims are returned, it is very easy to do. You simply need to implement `IMobileAuthClaimsHandler` and register it with the `MobileAuthenticationBuilder` like so:
120 |
121 | ```cs
122 | builder.AddMobileAuth(auth => {
123 | auth.AddMobileAuthClaimsHandler();
124 | });
125 | ```
126 |
127 | ## Run The Sample
128 |
129 | Each of the supported providers has a default callback `signin-{provider}`. For example, when configuring the domain & callback in the Google console for local testing with the demo app you would use `https://localhost:7172/signin-google`. Similarly you would use the localhost domain for Microsoft. However it is important to note that Apple does NOT support localhost. In the case of Apple, for local testing you will need to use a normal formatted (does not need to be real) domain. You can then update the hosts file on your local machine to map the domain to the localhost IP address.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
400 | appsettings.*.json
401 | *.p8
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/Images/dotnet_bot.svg:
--------------------------------------------------------------------------------
1 |
94 |
--------------------------------------------------------------------------------
/src/AvantiPoint.MobileAuth/MobileAuth.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.Eventing.Reader;
2 | using System.Net;
3 | using System.Reflection.PortableExecutable;
4 | using System.Security.Claims;
5 | using System.Text.RegularExpressions;
6 | using AvantiPoint.MobileAuth.Authentication;
7 | using AvantiPoint.MobileAuth.Configuration;
8 | using AvantiPoint.MobileAuth.Http;
9 | using AvantiPoint.MobileAuth.Stores;
10 | using Microsoft.AspNetCore.Authentication;
11 | using Microsoft.AspNetCore.Authentication.Cookies;
12 | using Microsoft.AspNetCore.Authentication.JwtBearer;
13 | using Microsoft.AspNetCore.Builder;
14 | using Microsoft.AspNetCore.Http;
15 | using Microsoft.AspNetCore.Mvc;
16 | using Microsoft.EntityFrameworkCore;
17 | using Microsoft.Extensions.Configuration;
18 | using Microsoft.Extensions.DependencyInjection;
19 | using Microsoft.Extensions.DependencyInjection.Extensions;
20 | using Microsoft.Extensions.Logging;
21 | using Microsoft.Extensions.Options;
22 | using Microsoft.IdentityModel.Tokens;
23 |
24 | namespace AvantiPoint.MobileAuth;
25 |
26 | public static class MobileAuth
27 | {
28 | private const string DefaultUserProfileRoute = "/api/users/me";
29 | private const string Tag = nameof(MobileAuth);
30 |
31 | public static WebApplicationBuilder AddMobileAuth(this WebApplicationBuilder appBuilder) =>
32 | appBuilder.AddMobileAuth(_ => { });
33 |
34 | public static WebApplicationBuilder AddMobileAuth(this WebApplicationBuilder appBuilder, Action configureAuthenticationBuilder)
35 | {
36 | var options = appBuilder.Configuration
37 | .GetSection("OAuth")
38 | .Get() ?? new OAuthLibraryOptions();
39 |
40 | if(string.IsNullOrEmpty(options.AuthPath))
41 | {
42 | options.AuthPath = "mobileauth";
43 | }
44 |
45 | if (string.IsNullOrEmpty(options.CallbackScheme))
46 | throw new ArgumentNullException(nameof(OAuthLibraryOptions.CallbackScheme));
47 |
48 | var authBuilder = appBuilder.Services
49 | .AddAuthentication(o =>
50 | {
51 | o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
52 | o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
53 | })
54 | .AddCookie(o =>
55 | {
56 | o.LoginPath = options.Signin;
57 | o.LogoutPath = options.Signout;
58 | });
59 | authBuilder.AddScheme(JwtBearerDefaults.AuthenticationScheme, null, options =>
60 | {
61 | options.TokenValidationParameters = new TokenValidationParameters
62 | {
63 | ValidateIssuer = true,
64 | ValidateAudience = true,
65 | ValidateLifetime = true,
66 | ValidateIssuerSigningKey = true,
67 | };
68 | })
69 | .Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>());
70 | options.Apple?.Configure(authBuilder, appBuilder);
71 | options.Google?.Configure(authBuilder);
72 | options.Microsoft?.Configure(authBuilder);
73 |
74 | appBuilder.Services.AddAuthorization()
75 | .AddSingleton(options)
76 | .AddSingleton(sp => sp.GetRequiredService())
77 | .AddHttpContextAccessor();
78 |
79 | var mobileAuthBuilder = new MobileAuthenticationBuilder(authBuilder);
80 | configureAuthenticationBuilder(mobileAuthBuilder);
81 | mobileAuthBuilder.ConfigureDefaultServices();
82 |
83 | return appBuilder;
84 | }
85 |
86 | public static WebApplication MapDefaultMobileAuthRoutes(this WebApplication app) =>
87 | app.MapMobileAuthRoute()
88 | .MapMobileAuthLogoutRoute()
89 | .MapMobileAuthUserClaimsRoute(DefaultUserProfileRoute);
90 |
91 | private static string GetPath(PathString path, string defaultValue) =>
92 | path.HasValue ? path.Value : defaultValue;
93 |
94 | public static WebApplication MapMobileAuthRoute(this WebApplication app, string name = "signin")
95 | {
96 | var options = app.Services.GetRequiredService();
97 |
98 | app.MapGet($"{options.Signin}{{scheme}}", Signin)
99 | .Produces(302)
100 | .ProducesProblem(204)
101 | .ProducesProblem(404)
102 | .WithTags(Tag)
103 | #if NET7_0_OR_GREATER
104 | .WithSummary("OAuth Login Endpoint.")
105 | .WithDescription("This will redirect to the appropriate OAuth provider such as Apple, Google or Microsoft based on the configured OAuth providers for the API.")
106 | .WithOpenApi()
107 | #endif
108 | .AllowAnonymous()
109 | .WithName(name)
110 | .WithDisplayName(name);
111 |
112 | return app;
113 | }
114 |
115 | public static WebApplication MapMobileAuthLogoutRoute(this WebApplication app, string name = "signout")
116 | {
117 | var options = app.Services.GetRequiredService();
118 |
119 | app.MapGet(options.Signout, Signout)
120 | .WithTags(Tag)
121 | #if NET7_0_OR_GREATER
122 | .WithSummary("Revokes user Token.")
123 | .WithDescription("This will revoke the user's token effectively logging out the user.")
124 | .WithOpenApi()
125 | #endif
126 | .WithName(name)
127 | .RequireAuthorization();
128 | return app;
129 | }
130 |
131 | public static WebApplication MapMobileAuthUserClaimsRoute(this WebApplication app, string routeTemplate, string name = "user-profile")
132 | {
133 | app.MapGet(routeTemplate, GetProfile)
134 | .WithTags(Tag)
135 | #if NET7_0_OR_GREATER
136 | .WithSummary("Provides dictionary of user claims.")
137 | .WithDescription("This will return the user claims for the currently authenticated user.")
138 | .WithOpenApi()
139 | #endif
140 | .RequireAuthorization()
141 | .WithName(name);
142 | return app;
143 | }
144 |
145 | private static Task GetProfile(HttpContext context, CancellationToken cancellationToken)
146 | {
147 | context.Response.StatusCode = 200;
148 | var claims = context.User.Claims.ToDictionary(x => GetKey(x), x => x.Value);
149 | return context.Response.WriteAsJsonAsync(claims);
150 | }
151 |
152 | private static string GetKey(Claim claim) =>
153 | claim.Properties.Any() ? claim.Properties.First().Value : claim.Type;
154 |
155 | private static async Task Signout(HttpContext context, CancellationToken cancellationToken)
156 | {
157 | var provider = context.User.FindFirstValue("provider");
158 | var tokenService = context.RequestServices.GetRequiredService();
159 | string? authHeader = context.Request.Headers.Authorization;
160 | if(!string.IsNullOrEmpty(authHeader))
161 | {
162 | var token = Regex.Replace(authHeader, "Bearer", string.Empty).Trim();
163 | if (!string.IsNullOrEmpty(token))
164 | {
165 | await tokenService.InvalidateToken(token);
166 | await context.Ok();
167 | }
168 | }
169 |
170 | await context.BadRequest();
171 | }
172 |
173 | private static async Task Signin(string scheme, ILoggerFactory loggerFactory, HttpContext context)
174 | {
175 | var logger = loggerFactory.CreateLogger(nameof(MobileAuth));
176 | if(scheme.Equals(CookieAuthenticationDefaults.AuthenticationScheme, StringComparison.InvariantCultureIgnoreCase) ||
177 | scheme.Equals(JwtBearerDefaults.AuthenticationScheme, StringComparison.InvariantCultureIgnoreCase))
178 | {
179 | logger.LogError($"'{scheme}' is an unsupported login provider.");
180 | context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
181 | context.Response.Headers.Add("Status", "Unsupported Scheme");
182 | await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails
183 | {
184 | Title = "Unsupported Scheme",
185 | Status = context.Response.StatusCode,
186 | Detail = "The Specified Scheme is not supported."
187 | });
188 | return;
189 | }
190 |
191 | var options = context.RequestServices.GetRequiredService();
192 | if(string.IsNullOrEmpty(options.CallbackScheme))
193 | {
194 | logger.LogError($"No Callback Scheme is configured for {scheme}.");
195 | context.Response.StatusCode = 204;
196 | context.Response.Headers.Add("Status", "No Callback Scheme is configured");
197 | await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails
198 | {
199 | Title = "No Callback Scheme is configured",
200 | Status = 204,
201 | Detail = "The web application has not been configured with a proper callback scheme. Please check your app's configuration.",
202 | });
203 | return;
204 | }
205 |
206 | var ignoreSchemes = new[]
207 | {
208 | CookieAuthenticationDefaults.AuthenticationScheme,
209 | JwtBearerDefaults.AuthenticationScheme
210 | };
211 |
212 | var provider = context.RequestServices.GetRequiredService();
213 | var schemes = await provider.GetAllSchemesAsync();
214 | if(schemes is null || !schemes.Any(x => !ignoreSchemes.Contains(x.Name)))
215 | {
216 | logger.LogError($"No authentication schemes defined.");
217 | context.Response.StatusCode = 204;
218 | context.Response.Headers.Add("Status", "No Authentication Schemes are configured");
219 | await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails
220 | {
221 | Title = "No Authentication Schemes Available",
222 | Status = 204,
223 | Detail = "The web application has not been configured with any Authentication Providers. Please check your app's configuration.",
224 | });
225 | return;
226 | }
227 |
228 | var authenticationScheme = schemes.FirstOrDefault(x => x.Name.Equals(scheme, StringComparison.InvariantCultureIgnoreCase));
229 |
230 | if(authenticationScheme is null)
231 | {
232 | logger.LogError($"No authentication scheme found matching {scheme}.");
233 | context.Response.StatusCode = 404;
234 | await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails
235 | {
236 | Title = "Authentication Scheme not found",
237 | Status = 404,
238 | Detail = $"The web application has not been configured with the scheme '{scheme}'.",
239 | });
240 | return;
241 | }
242 |
243 | var auth = await context.AuthenticateAsync(authenticationScheme.Name);
244 |
245 | if (!auth.Succeeded
246 | || auth?.Principal == null
247 | || !auth.Principal.Identities.Any(id => id.IsAuthenticated)
248 | || string.IsNullOrEmpty(auth.Properties.GetTokenValue("access_token")))
249 | {
250 | // Not authenticated, challenge
251 | await context.ChallengeAsync(authenticationScheme.Name);
252 | return;
253 | }
254 |
255 | var handler = context.RequestServices.GetRequiredService();
256 | var claims = await handler.GenerateClaims(context, auth, authenticationScheme.Name);
257 | if(!claims.Any())
258 | {
259 | context.Response.StatusCode = 401;
260 | return;
261 | }
262 |
263 | var tokenService = context.RequestServices.GetRequiredService();
264 | var outputClaims = new Dictionary
265 | {
266 | { "access_token", await tokenService.BuildToken(claims) },
267 | { "id_token", claims.FindFirstValue("id_token") ?? string.Empty },
268 | { "expires_in", claims.FindFirstValue("expires_in") ?? string.Empty }
269 | };
270 |
271 | // Build the result url
272 | var url = GetRedirectUri(options.CallbackScheme, outputClaims);
273 |
274 | // Redirect to final url
275 | context.Response.Redirect(url);
276 | }
277 |
278 | private static string GetRedirectUri(string callbackScheme, Dictionary claims)
279 | {
280 | var qs = claims.Where(x => !string.IsNullOrEmpty(x.Value) && x.Value != "-1")
281 | .Select(kvp => $"{kvp.Key}={kvp.Value}");
282 | return $"{callbackScheme}://auth?{string.Join("&", qs)}";
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/sample/DemoMobileApp/Resources/Styles/Styles.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
10 |
11 |
15 |
16 |
21 |
22 |
25 |
26 |
47 |
48 |
63 |
64 |
82 |
83 |
102 |
103 |
122 |
123 |
128 |
129 |
147 |
148 |
165 |
166 |
170 |
171 |
191 |
192 |
207 |
208 |
226 |
227 |
230 |
231 |
252 |
253 |
273 |
274 |
280 |
281 |
300 |
301 |
304 |
305 |
333 |
334 |
352 |
353 |
357 |
358 |
370 |
371 |
376 |
377 |
383 |
384 |
385 |
--------------------------------------------------------------------------------