代码之家  ›  专栏  ›  技术社区  ›  Dave Barnett

在本地主机上运行IdentityServer的Maui应用程序上测试身份验证时出现问题

  •  0
  • Dave Barnett  · 技术社区  · 1 年前

    我需要建立一个。NET 7 MAUI应用程序,该应用程序在上进行身份验证。NET 7 ASP。NET Core应用程序运行Duende IdentityServer(版本6.2.3)。我从一个概念验证应用程序开始,但当我在localhost上运行IdentityServer时,我在测试它时遇到了问题。

    我的代码基于这里的一个示例应用程序 https://github.com/DuendeSoftware/Samples/tree/main/various/clients/Maui/MauiApp2 。而IdentityServer代码基本上是一个开箱即用的IdentityServer,它有一个用ASP完成的标准ui。NET核心剃须刀页面代码。

    我曾尝试使用安卓模拟器进行测试,该模拟器使用ngrok生成的url调用IDP,但我收到了以下错误:

    系统InvalidOperationException:'加载发现文档时出错:终结点位于与颁发机构不同的主机上:https://localhost:5001/.well-已知/openid配置/jwks'

    也就是说,我的权威有点像 https://4cec-81-134-5-170.ngrok.io 但是发现文档上的所有url仍然使用localhost url,因此不匹配。

    我已经尝试在安卓模拟器上测试并使用权威 https://10.0.2.2 但这在以下情况下失败:

    系统InvalidOperationException:'加载发现文档时出错:连接到时出错 https://10.0.2.2/.well-known/openid-configuration .java.security.cert.CertPathValidatorException:找不到证书路径的信任锚点..'

    由于我只是在这里进行开发测试,所以我设置了本地IDP以使用http(而不是https),并使用 http://10.0.2.2 但失败的原因如下:

    系统InvalidOperationException:'加载发现文档时出错:连接到时出错 http://10.0.2.2/.well-known/openid-configuration 需要.HTTPS。'

    我想知道是否有一种方法可以让我的代码通过localhost测试(使用移动应用程序或设备的模拟器)来工作。当我说我工作时,我的意思是当 _client.LoginAsync() 在主页上调用时,上面提到的3个错误不会发生,您会看到成功消息。我认为这可以通过ngrok问题的解决方案或让Android信任ASP来实现。NET Core localhost证书或其他什么。我发现了这个 https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/local-web-services?view=net-maui-7.0#bypass-the-certificate-security-check 。这解释了在连接到localhost时如何通过将自定义HttpMessageHandler传递到httpclient来绕过证书安全检查。使用OidcClient时可以执行类似的操作吗?

    Source code for OidcClient found here

    我也在这里找到了解决方案 https://github.com/dotnet/maui/discussions/8131 但我无法使这4个选项中的任何一个对我有效。要么它们不启用localhost测试,要么它们不起作用。

    以下是我的代码的关键部分:

    IDP代码

    我在我的Program.cs代码中添加身份服务器,如下所示

    builder.Services.AddIdentityServer(options =>
            {             
                options.EmitStaticAudienceClaim = true;
            })
            .AddInMemoryIdentityResources(Config.IdentityResources)
            .AddInMemoryApiScopes(Config.ApiScopes)
            .AddInMemoryClients(Config.Clients)
            .AddTestUsers(TestUsers.Users);
    

    这是正在引用的Config类

    using Duende.IdentityServer;
    using Duende.IdentityServer.Models;
    
    namespace MyApp.IDP;
    
    public static class Config
    {
        public static IEnumerable<IdentityResource> IdentityResources =>
            new IdentityResource[]
            { 
                new IdentityResources.OpenId(),
                new IdentityResources.Profile()
            };
    
        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
                { };
    
        public static IEnumerable<Client> Clients =>
            new Client[] 
                { 
                    new Client()
                    {
                        ClientName = My App Mobile",
                        ClientId = "myappmobile.client",
                        AllowedGrantTypes = GrantTypes.Code,
                        RedirectUris = {
                            "myapp://callback" 
                        },
                        PostLogoutRedirectUris = { 
                            "myapp://callback"
                        },
                        AllowedScopes = new List<string>
                        {
                            IdentityServerConstants.StandardScopes.OpenId,
                            IdentityServerConstants.StandardScopes.Profile                       
                        }
                    }
                };
    }
    

    客户端移动代码

    我这样注册我的OidcClient

    var options = new OidcClientOptions
    {       
        Authority = "https://10.0.2.2",
        ClientId = "myappmobile.client",        
        RedirectUri = "myapp://callback",
        Browser = new MauiAuthenticationBrowser()
    };
    
    builder.Services.AddSingleton(new OidcClient(options));
    

    MauiAuthenticationBrowser的代码如下

    using IdentityModel.Client;
    using IdentityModel.OidcClient.Browser;
    
    namespace MyFirstAuth;
    
    public class MauiAuthenticationBrowser : IdentityModel.OidcClient.Browser.IBrowser
    {
        public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
        {
            try
            {
                var result = await WebAuthenticator.Default.AuthenticateAsync(
                    new Uri(options.StartUrl),
                    new Uri(options.EndUrl));
    
                var url = new RequestUrl("myapp://callback")
                    .Create(new Parameters(result.Properties));
    
                return new BrowserResult
                {
                    Response = url,
                    ResultType = BrowserResultType.Success
                };
            }
            catch (TaskCanceledException)
            {
                return new BrowserResult
                {
                    ResultType = BrowserResultType.UserCancel
                };
            }
        }
    }
    

    该应用程序只是一个带有登录按钮的页面。以下是该页面的代码

    using IdentityModel.OidcClient;
    
    namespace MyFirstAuth;
    public partial class MainPage
    {
        private readonly OidcClient _client;
    
        public MainPage(OidcClient client)
        {
            InitializeComponent();
            _client = client;
        }
    
        private async void OnLoginClicked(object sender, EventArgs e)
        {
            var result = await _client.LoginAsync();
    
            if (result.IsError)
            {
                editor.Text = result.Error;
                return;
            }
    
            editor.Text = "Success!";
        }
    }
    
    0 回复  |  直到 1 年前
        1
  •  2
  •   Dave Barnett    1 年前

    以下是如何使用https进行测试,如果您想要http的答案,请参阅 dreamboatDevs answer

    OidcClient确实使用HttpClient,因此可以使用建议的方法 in the Microsoft docs

    如果检查代码 OidcClientOptions 有一个HttpClientFactory属性如下所示

    
    public Func<OidcClientOptions, HttpClient> HttpClientFactory { get; set; }
    
    

    因此,您可以将用于注册OidcClient的代码更改为

    
    Func<OidcClientOptions, HttpClient> httpClientFactory = null;
    
    #if DEBUG
            httpClientFactory = (options) =>
            {
                var handler = new HttpsClientHandlerService();
                return new HttpClient(handler.GetPlatformMessageHandler());
            };
    #endif
    
    var options = new OidcClientOptions
    {       
        Authority = "https://10.0.2.2",
        ClientId = "myappmobile.client",        
        RedirectUri = "myapp://callback",
        Browser = new MauiAuthenticationBrowser(),
        HttpClientFactory = httpClientFactory
    };
    
    builder.Services.AddSingleton(new OidcClient(options));
    
    
    
    

    注意#if DEBUG,因为这段代码只在开发中需要。当httpClientFactory为null时,OidcClient将只是新建一个正常的HttpClient。

    的代码 HttpsClientHandlerService 直接来自 the Microsoft docs 这是吗

    
    public class HttpsClientHandlerService
    {
        public HttpMessageHandler GetPlatformMessageHandler()
        {
    #if ANDROID
            var handler = new Xamarin.Android.Net.AndroidMessageHandler();
            handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
            {
                if (cert != null && cert.Issuer.Equals("CN=localhost"))
                    return true;
                return errors == System.Net.Security.SslPolicyErrors.None;
            };
            return handler;
    #elif IOS
            var handler = new NSUrlSessionHandler
            {
                TrustOverrideForUrl = IsHttpsLocalhost
            };
            return handler;
    #else
            throw new PlatformNotSupportedException("Only Android and iOS supported.");
    #endif
        }
    
    #if IOS
        public bool IsHttpsLocalhost(NSUrlSessionHandler sender, string url, Security.SecTrust trust)
        {
            if (url.StartsWith("https://localhost"))
                return true;
            return false;
        }
    #endif
    }
    
    

    正如您所看到的,当在本地主机上以调试模式进行开发时,会根据需要自动信任证书。

        2
  •  2
  •   dreamboatDev    1 年前

    我将以新类的形式创建一个额外的包装器,在里面配置您的服务。证书问题(http或https)使用策略配置解决:

     Policy = new IdentityModel.OidcClient.Policy()
                        {
                            Discovery = new IdentityModel.Client.DiscoveryPolicy()
                            {
                                RequireHttps = config.GetRequiredSection("IdentityServer").GetValue<bool>("RequireHttps")
                            }
                        }
    

    详细的客户端移动示例:

    //In this class, you can add any additional logic and use it as a kind of decorator
    
    public class Auth0Client
        {
            //Your real service. 
            private readonly OidcClient oidcClient;
    
            public Auth0Client(Auth0ClientOptions options)
            {
                oidcClient = new OidcClient(new OidcClientOptions
                {
                    Authority = options.Authority,
                    ClientId = options.ClientId,
                    ClientSecret = options.ClientSecret,
                    Scope = options.Scope,
                    RedirectUri = options.RedirectUri,
                    PostLogoutRedirectUri = options.PostLogoutRedirectUri,
                    Policy = options.Policy,
                    Browser = options.Browser
                });
            }
    
            public IdentityModel.OidcClient.Browser.IBrowser Browser
            {
                get
                {
                    return oidcClient.Options.Browser;
                }
                set
                {
                    oidcClient.Options.Browser = value;
                }
            }
    
            public async Task<LoginResult> LoginAsync()
            {
                return await oidcClient.LoginAsync();
            }
    
            public async Task<LogoutResult> LogoutAsync(string identityToken)
            {
                LogoutResult logoutResult = await oidcClient.LogoutAsync(new LogoutRequest { IdTokenHint = identityToken });
                return logoutResult;
            }
        }
    
    
    public class Auth0ClientOptions
        {
            public Auth0ClientOptions()
            {
                Browser = new WebBrowserAuthenticator();
            }
    
            public string Authority { get; set; }
    
            public string ClientId { get; set; }
            public string ClientSecret { get; set; }
    
            public string RedirectUri { get; set; }
    
            public string PostLogoutRedirectUri { get; set; }
    
            public string Scope { get; set; }
    
            public Policy Policy { get; set; }
            public IdentityModel.OidcClient.Browser.IBrowser Browser { get; set; }
        }
    
    public class WebBrowserAuthenticator : IdentityModel.OidcClient.Browser.IBrowser
        {
            public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
            {
                try
                {
                    WebAuthenticatorResult result = await WebAuthenticator.Default.AuthenticateAsync(
                        new Uri(options.StartUrl),
                        new Uri(options.EndUrl));
    
                    var url = new RequestUrl(options.EndUrl)
                        .Create(new Parameters(result.Properties));
    
                    return new BrowserResult
                    {
                        Response = url,
                        ResultType = BrowserResultType.Success
                    };
                }
                catch (TaskCanceledException)
                {
                    return new BrowserResult
                    {
                        ResultType = BrowserResultType.UserCancel,
                        ErrorDescription = "Login canceled by the user."
                    };
                }
            }
        }
    

    配置服务

     builder.Services.AddScoped(new Auth0Client(new Auth0ClientOptions()
                {
                    Authority = config.GetRequiredSection("IdentityServer:Authority").Value,
                    ClientId = config.GetRequiredSection("IdentityServer:ClientId").Value,
                    ClientSecret = config.GetRequiredSection("IdentityServer:ClientSecret").Value,
                    Scope = config.GetRequiredSection("IdentityServer:Scope").Value,
                    RedirectUri = config.GetRequiredSection("IdentityServer:RedirectUri").Value,
                    PostLogoutRedirectUri = config.GetRequiredSection("IdentityServer:PostLogoutRedirectUri").Value,
                    Policy = new IdentityModel.OidcClient.Policy()
                    {
                        Discovery = new IdentityModel.Client.DiscoveryPolicy()
                        {
                            RequireHttps = config.GetRequiredSection("IdentityServer").GetValue<bool>("RequireHttps")
                        }
                    }
                }));
    

    使用服务

    public partial class MainPage : ContentPage
        {       
            private readonly Auth0Client auth0Client;
    
            public MainPage(Auth0Client client)
            {
                InitializeComponent();
                auth0Client = client;    
            }
    
            private async void OnLoginClicked(object sender, EventArgs e)
            {
                var loginResult = await auth0Client.LoginAsync();                  
            }
    
            private async void OnLogoutClicked(object sender, EventArgs e)
            {
                var logoutResult = await auth0Client.LogoutAsync("");          
            }
    

    我还建议使用secrets.json来存储设置(URI等)。YouTube上有一段关于如何将他们与毛伊岛项目联系起来的视频。视频名为: “.Net MAUI和Xamarin Forms从secrets.json或appsettings.json获取设置”

    最重要的是,在包装器中实现try-catch块会更容易

    如果要将服务直接注入到页面构造函数中,请不要忘记为其指定依赖项

    builder.Services.AddScoped<MainPage>();
    

    settings.json

    {
      "IdentityServer": {
        "Authority": "http://test-site.com",
        "ClientId": "mobile-client",
        "ClientSecret" : "qwerty123*",
        "Scope": "openid profile",
        "RedirectUri": "mauiclient://signin-oidc",
        "PostLogoutRedirectUri": "mauiclient://signout-callback-oidc",
        "RequireHttps" :  "false"
      }
    }
    

    如果使用http协议,则添加到清单(Android)

    <application 
        android:usesCleartextTraffic="true">
    </application>
    
    推荐文章