【问题标题】:Connecting with Javascript to Web API protected by IdentityServer3使用 Javascript 连接到受 IdentityServer3 保护的 Web API
【发布时间】:2016-12-05 05:29:36
【问题描述】:

我有一个带有嵌入式 IdentityServer3 的 Asp.NET MVC / WebAPI 项目。

我希望 MVC 和 WebAPI 都受到 IdentityServer 的保护。所以我在 MVC 控制器和 API 控制器上都使用了 Authorize 属性。

浏览到我的测试页面(受保护)时,我被重定向到 IdentityServer 登录页面。我输入我的用户名和密码并获得身份验证并重定向回来。 在页面上,我有一个按钮触发从 javascript 到我受保护的 API 的 GET,我的访问令牌在授权标头中。但在这里它失败了 401 Unauthorized。 我通过使用 Razor 将其呈现到页面来获取 javascript 的访问令牌。

我在 IdentityServer 中有 1 个客户端设置为混合流。 MVC 使用 cookie,而 API 使用不记名令牌。 在我的 API HttpConfiguration 上,我设置了 SuppressDefaultHostAuthentication。如果我删除该行一切正常,但它会使用我不想要的 API 的 cookie。

我现在只使用 HTTP 和 RequireSsl=false 以避免潜在的证书问题。

我已经尝试了好几天才能让它工作,但我一无所获。 我什至不知道如何调试它才能找到 401 的原因。

到现在为止,当我搜索帮助时,我几乎可以识别出 google 建议的每个页面。

任何想法可能是什么或如何调试它?

这是我的 Startup.cs

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.Trace()
            .CreateLogger();

        // MVC client

        string authBaseAddress = "http://localhost:50319/identity";
        string tokenEndpoint = authBaseAddress + "/connect/token";
        string userInfoEndpoint = authBaseAddress + "/connect/userinfo";

        string redirectUri = "http://localhost:50319/";

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = "Cookies"
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            ClientId = "hybrid_clients",
            Authority = authBaseAddress,
            RedirectUri = redirectUri,

            ResponseType = "code id_token token",

            Scope = "openid profile roles sampleApi offline_access", 

            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name",
                RoleClaimType = "role"
            },

            SignInAsAuthenticationType = "Cookies",

            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthorizationCodeReceived = async n =>
                {
                    // use the code to get the access and refresh token
                    var tokenClient = new TokenClient(
                        tokenEndpoint,
                        "hybrid_clients",
                        "secret");

                    var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);

                    if (tokenResponse.IsError)
                    {
                        throw new Exception(tokenResponse.Error);
                    }

                    // use the access token to retrieve claims from userinfo
                    var userInfoClient = new UserInfoClient(
                        new Uri(userInfoEndpoint),
                        tokenResponse.AccessToken);

                    var userInfoResponse = await userInfoClient.GetAsync();

                    // create new identity
                    var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
                    id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);

                    id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
                    id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
                    id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
                    id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
                    id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));

                    n.AuthenticationTicket = new AuthenticationTicket(
                        new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
                        n.AuthenticationTicket.Properties);
                },

                // Attach the id_token for the logout roundtrip to IdentityServer
                RedirectToIdentityProvider = n =>
                {
                    if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                    {
                        var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                        if (idTokenHint != null)
                        {
                            n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                        }
                    }

                    return Task.FromResult(0);
                }
            }
        });

        AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
        JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();

        // web api

        app.Map("/api", a =>
        {
            var config = new HttpConfiguration();

            a.UseCors(CorsOptions.AllowAll);

            a.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
            {
                //AuthenticationMode = AuthenticationMode.Active,
                Authority = authBaseAddress,
                RequiredScopes = new[] { "sampleApi" },
                DelayLoadMetadata = true
            });

            config.SuppressDefaultHostAuthentication();
            config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

            config.MapHttpAttributeRoutes();

            a.UseWebApi(config);
        });

        // Identity server

        app.Map("/identity", idsrvApp =>
        {
            idsrvApp.UseCors(CorsOptions.AllowAll);

            idsrvApp.UseIdentityServer(new IdentityServerOptions
            {
                SiteName = "Embedded IdentityServer",
                SigningCertificate = LoadCertificate(),

                Factory = new IdentityServerServiceFactory()
                            .UseInMemoryUsers(Users.Get())
                            .UseInMemoryClients(Clients.Get())
                            .UseInMemoryScopes(Scopes.Get()),

                AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions()
                {
                    EnablePostSignOutAutoRedirect = true // Automatically redirects back to the client on signout
                },

                RequireSsl = false,

            });
        });

    }

    X509Certificate2 LoadCertificate()
    {
        return new X509Certificate2(
            string.Format(@"{0}\bin\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
    }
}

我的客户

new Client
            {
                Enabled = true,
                ClientName = "Hybrid Clients",
                ClientId = "hybrid_clients",
                Flow = Flows.Hybrid,

                //AllowAccessTokensViaBrowser = false,

                RedirectUris = new List<string>
                {
                    "http://localhost:50319/"
                },
                PostLogoutRedirectUris = new List<string>
                {
                    "http://localhost:50319/"
                },

                AllowedScopes = new List<string>
                {
                    "openid",
                    "profile",
                    "email",
                    "roles",
                    "address",
                    "all_claims",
                    "sampleApi",
                    "offline_access"
                },

                ClientSecrets = new List<Secret>
                {
                    new Secret("secret".Sha256())
                },

                AccessTokenType = AccessTokenType.Reference,
                LogoutSessionRequired = true

            },

我的范围

public static class Scopes
{
    public static IEnumerable<Scope> Get()
    {
        var scopes = new List<Scope>
        {
            StandardScopes.OpenId,
            StandardScopes.Profile,
            StandardScopes.Email,
            StandardScopes.Address,
            StandardScopes.OfflineAccess,
            StandardScopes.RolesAlwaysInclude,
            StandardScopes.AllClaims,

            new Scope
            {
                Enabled = true,
                Name = "roles",
                Type = ScopeType.Identity,
                Claims = new List<ScopeClaim>
                {
                    new ScopeClaim("role")
                }
            },
            new Scope
            {
                Enabled = true,
                DisplayName = "Sample API",
                Name = "sampleApi",
                Description = "Access to a sample API",
                Type = ScopeType.Resource
            }
        };

        return scopes;
    }
}

我的 API

[Authorize]
public class SecuredApiController : ApiController
{
    public IHttpActionResult Get()
    {
        var user = User as ClaimsPrincipal;
        var claims = from c in user.Claims
                     select new
                     {
                         type = c.Type,
                         value = c.Value
                     };

        return Json(claims);
    }
}

我的 Razor 视图的一部分

<button data-bind="click:callApi">Call API</button>
<span data-bind="text:apiResult"></span>

<script>
    $(function() {
        ko.myViewModel = new ClientAppViewModel('@ViewData["access_token"]');
        ko.applyBindings(ko.myViewModel);
    });
</script>

我调用 SecuredApi 的 JavaScript (KnockoutJS)

function ClientAppViewModel(accessToken) {
    var self = this;

    self.accessToken = accessToken;

    self.apiResult = ko.observable('empty');

    self.callApi = function () {
        console.log('CallApi');
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "http://localhost:50319/api/SecuredApi");
        xhr.onload = function () {
            self.apiResult(JSON.stringify(JSON.parse(xhr.response), null, 2));
        };
        xhr.setRequestHeader("Authorization", "Bearer " + self.accessToken);
        xhr.send();
    }
}

【问题讨论】:

    标签: asp.net-mvc asp.net-web-api oauth-2.0 identityserver3


    【解决方案1】:

    API 的 Katana 日志是您想要的。通过这些您将了解 API 返回 401 的原因。

    您可以使用 Microsoft.Owin 日志源访问这些内容(请参阅 Katana Documentation

    【讨论】:

    • 我已经激活了 Katana 日志,但不幸的是,当我得到 401 时,没有写入任何内容。我知道日志有效,因为当我调查另一个问题时,我得到了其他信息。
    • 那么我认为您可能需要将发生 401 的 API 中的代码放入您的问题中。我假设您正在使用将日志输出到 katana 日志的 IdentityServer 不记名令牌中间件。
    • 感谢您抽出宝贵时间。我已将所有代码添加到我的问题中。是的,我正在使用 IdentityServer 不记名令牌中间件
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-29
    • 2019-09-26
    • 2017-02-10
    • 1970-01-01
    • 1970-01-01
    • 2020-02-19
    相关资源
    最近更新 更多