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

rryofs0p  于 2023-03-09  发布在  .NET
关注(0)|答案(1)|浏览(120)

我需要构建一个.NET 7 MAUI应用程序,该应用程序在运行Duende IdentityServer(版本6.2.3)的.NET 7 ASP.NET核心应用程序上进行身份验证。我从一个概念验证应用程序开始,但在本地主机上运行IdentityServer时,我在测试它时遇到了问题。
我的代码是基于一个示例应用程序来做这件事的,可以在这里找到https://github.com/DuendeSoftware/Samples/tree/main/various/clients/Maui/MauiApp2。而IdentityServer代码几乎是一个开箱即用的IdentityServer,带有一个标准的ui,用ASP.NET核心剃刀页面代码完成。
我试过使用android模拟器进行测试,该模拟器使用ngrok生成的url调用IDP,但我得到了以下错误:
系统操作无效异常:'加载发现文档时出错:终结点与颁发机构位于不同的主机上:https://localhost:5001/. well-known/openid-configuration/jwks '
也就是说,我的权限类似于https://4cec-81-134-5-170.ngrok.io,但发现文档上的所有url仍然使用localhost url,因此不匹配。
我试过在android模拟器上测试,并使用https://10.0.2.2权威软件,但失败原因如下:
系统操作无效异常:'加载发现文档时出错:连接到https://10.0.2.2/.well-known/openid-configuration.java.security.cert时出错。证书路径验证器异常:找不到证书路径的信任锚..'
由于我只是在这里进行开发测试,所以我设置了本地IDP以使用http(而不是https),并使用http://10.0.2.2进行了测试,但失败原因如下:
系统操作无效异常:'加载发现文档时出错:连接到http://10.0.2.2/.well-known/openid-configuration时出错。需要HTTPS。'
我想知道是否有一种方法可以让我的代码通过测试localhost来工作(使用移动的应用程序或设备的模拟器).当我说我工作,我的意思是当_client.LoginAsync()是在主页上调用上述3个错误不会发生,你会看到成功的消息.我认为这可以通过解决ngrok问题或让Android信任ASP.NET核心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个选项中的任何一个为我工作,要么它们不支持本地主机测试,要么它们不工作。
下面是我的代码的关键部分:

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!";
    }
}
3xiyfsfu

3xiyfsfu1#

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

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上有一个视频介绍如何将它们连接到Maui项目,视频名为:“.Net MAUI和Xamarin表单正在从secrets.json或appsettings.json获取设置”
最重要的是,在 Package 器中实现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"
  }
}

相关问题