I'm not getting a scope checkbox when the Auth

2019-02-26 07:04发布

问题:

UPDATE 2: If I change my controller Authorize tag from this

[Authorize]

to this

[Authorize(Roles = "Read")]

then I get the checkbox for scope selection and the ajax token request contains the correct scope and completes successfully. I still end up with a red exclamation mark however. It looks like Swagger or Swashbuckle is requiring that the roles match the scope definitions? Is it possible to use the application flow with no Roles defined when using Swashbuckle? And if so how do you get that to work? Do I have to manually set the scope in the operation filter class? If it's not possible to use Swashbuckle without listing Roles in the Authorize tag, then I need to know how to assign clients roles in IdentityServer3.

UPDATE 3 If I change the Operation Filter to something like this the scope appears, but after selecting the scope and clicking on Authorize, the page just reloads. The ajax authorization was sent successfully first. This is closer, but the authorization still doesn't stick (not sure what term to use here, but stick seems to sum it up.) How do I get the authorization to stick?

public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{

    var scopes = new List<string>() { "Read" };

    if (scopes.Any())
    {
        if (operation.security == null)
            operation.security = new List<IDictionary<string, IEnumerable<string>>>();

        var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
        {
            { "oauth2", scopes }
        };

        operation.security.Add(oAuthRequirements);
    }
}

Original Post I'm trying to configure Swashbuckle to allow clients to test a REST service that is protected by an OAuth2 client credentials flow. The toggle never appears on the page, should it?, but I do get a red circle with an exclamation mark telling me the resource is not protected. I'm using the nuget package Swashbuckle.Core Version 5.4.0. The answer here Enable Oauth2 client credentials flow in Swashbuckle seems to follow what I've done, and have used the AssignOAuth2SecurityRequirements class verbatim. I haven't injected any javascript and don't believe I have to since my authorization scheme is fairly standard. When I remove the Authorize key word on the Controller that method no longer has the red exclamation in the Swagger UI which I'm hoping means I'm close, but I'm not finding the missing link. Since this Flow is "application" and I only have one scope I wanted to make sure that it looks configured correctly and the clientSecret is loaded in the correct spot.

UPDATE 1 I've been able to debug the AJAX call and can see that the scope is not set and therefor not sent in the request. Why is the scope not being set? Why don't I have a checkbox to select the scope?

Here is my SwaggerConfig.cs

public class SwaggerConfig
{
    public static void Register()
    {
        var thisAssembly = typeof(SwaggerConfig).Assembly;

        GlobalConfiguration.Configuration
            .EnableSwagger(c =>
            {
                c.SingleApiVersion("v1", "waRougeOneApp");
                c.OAuth2("oauth2")
                    .Description("OAuth2 Client Credentials Grant Flow")
                    .Flow("application")
                    .TokenUrl("https://securitydev.rougeone.com/core/connect/token")
                    .Scopes(scopes =>
                    {
                        scopes.Add("Read", "Read access to protected resources");
                    });
                c.IncludeXmlComments(GetXmlCommentsPath());
                c.UseFullTypeNameInSchemaIds();
                c.DescribeAllEnumsAsStrings();
                c.OperationFilter<AssignOAuth2SecurityRequirements>();
            })
            .EnableSwaggerUi(c =>
            {
                c.EnableOAuth2Support(
                    clientId: "client_id",
                    clientSecret: "client_secret",
                    realm: "swagger-realm",
                    appName: "Swagger UI"
                );
            });
    }

    protected static string GetXmlCommentsPath()
    {
        return System.String.Format(@"{0}bin\\waRougeOne.xml", System.AppDomain.CurrentDomain.BaseDirectory);
    }
}

And the AssignOAuth2SecurityRequirements class is

public class AssignOAuth2SecurityRequirements : IOperationFilter
{
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        var authorized = apiDescription.ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>();
        if (!authorized.Any()) return;

        if (operation.security == null)
            operation.security = new List<IDictionary<string, IEnumerable<string>>>();

        var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
  {
      {"oauth2", Enumerable.Empty<string>()}
  };

        operation.security.Add(oAuthRequirements);
    }
}

I've been trying to find a working example with a client credentials flow without success, so I'm not 100% sure I'll see a toggle button when everything is working correctly. In the examples for the implicit flow if you hover over the red exclamation circle you see the grant types listed, clicking on the red exclamation circle shows the options for scopes listed out in which you select one and then click authorize and it comes back with a blue exclamation.

For me I never get a checkbox to select a scope, but I've only defined one scope. What am I doing wrong? I found this while debugging the swagger ui JavaScript which seems to point to having all the data it needs?

authorizations
:
null
auths
:
Array[1]
0
:
Object
name
:
"oauth2"
type
:
"oauth2"
value
:
Object
description
:
"OAuth2 Client Credentials Grant Flow"
flow
:
"application"
scopes
:
Object
Read
:
"Read access to protected resources"
__proto__
:
Object
tokenUrl
:
"https://security.starrwarrs.com/core/connect/token"
type
:
"oauth2"
__proto__
:
Object
__proto__
:
Object
length
:
1
__proto__
:
Array[0]

回答1:

These are the steps which we have done and worked:

  1. In the SwaggerConfig file, add the below settings:
c.OAuth2("oauth2")
 .Description("OAuth2 Implicit Grant") 
 .Flow("implicit")
 .AuthorizationUrl(swaggerConfigurations["IssuerUri"].ToString())
 .Scopes(scopes =>
  {
    scopes.Add("user_impersonation", "Access REST API");
  });

The attributes are:

  • Name of the authorization scheme (oauth2 in the above sample)
  • Description of the authorization scheme
  • Flow – Type of grant to be used
  • Authorization Url – Should be the STS url (eg: https://auth2.test.com/oauth2/authorize)
  • Scopes – The scope name

II. In the SwaggerConfig file, add the below settings also under the swagger ui configuration section:

c.EnableOAuth2Support(swaggerConfigurations["ClientId"].ToString(), string.Empty, swaggerConfigurations["RedirectUri"].ToString(), "Swagger", " ", new Dictionary<string, string> { { "resource", WebApi.Utility.GetAudiences().First() } });

The method accepts the below parameters:

  • clientId – This should be client ID for swagger configured in Security Token Service
  • clientSecret – This should be client secret key. This is required only in case of Code grant type
  • realm – This should be the redirect url (this should be [base address] + swagger/ui/o2c-html)
  • appName – This should be swagger
  • scopeSeperator – This is not required to be passed if there is only scope
  • additionalQueryStringParams – This should have the list of valid audiences and this corresponds to the resource for which the token is issued.

III. Create a new Operation Filter in the web api project as shown below:

public class AssignOperationFilters : IOperationFilter
    {              
        public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
        {     
            string clientId = "clientID";
            if (apiDescription != null)
            {
                var actFilters = apiDescription.ActionDescriptor.GetFilterPipeline();

                var allowsAnonymous = actFilters.Select(f => f.Instance).OfType<OverrideAuthorizationAttribute>().Any();
                if (allowsAnonymous)
                {
                    return; // must be an anonymous method
                }
            }

            if (operation != null)
            {
                if (operation.security == null)
                {
                    operation.security = new List<IDictionary<string, IEnumerable<string>>>();
                }

                var authRequirements = new Dictionary<string, IEnumerable<string>>
                {
                    { "oauth2", new List<string> { clientId } }
                };

                operation.security.Add(authRequirements);
            }
        }
    }

This class will be used to bind the OAuth scopes to the individual operations

IV. Add the above filter in the swagger config file (see code below)

c.OperationFilter<AssignOperationFilters>();

V. Configure the Client ID, Secret, Redirect Url and Resource in Security Token Service

VI. In the Web API project, if there is an index.html being used to inject API specific UI fields/styles, then make sure that all the javascript code is kept intact with the Swashbuckle version of the index.html file (as provided in the location - https://github.com/domaindrivendev/Swashbuckle/blob/master/Swashbuckle.Core/SwaggerUi/CustomAssets/index.html)



回答2:

Solution!! The last part was the hardest to figure out, which I finally did with the help of the Chrome Developer tools that showed a little red X on the network tag showing the following error message:

XMLHttpRequest cannot load http://security.RogueOne.com/core/connect/token. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:62561' is therefore not allowed access.

The error message finally connected the dots below, until then the on OAuthComplete complete JavaScript function would be called, but with no token. The network tab show "This request has no response data available", but I'd see a Content-Length in the Response headers with a content-type of Json. Fiddler also showed the response which looked like (and was) well formed JSON.

I described this error here Swagger UI not parsing reponse which was due to IdentityServer3 correctly not adding a response header of "Access-Control-Allow-Origin:http://localhost:62561" You can force IdentityServer3 to send that header by updating you client creation to be the following:

new Client
{
    ClientName = "SwaggerUI",
    Enabled = true,
    ClientId = "swaggerUI",
    ClientSecrets = new List<Secret>
    {
        new Secret("PasswordGoesHere".Sha256())
    },
    Flow = Flows.ClientCredentials,
    AllowClientCredentialsOnly = true,
    AllowedScopes = new List<string>
    {
        "Read"
    },

    Claims = new List<Claim>
    {
        new Claim("client_type", "headless"),
        new Claim("client_owner", "Portal"),
        new Claim("app_detail", "allow")
    },
    PrefixClientClaims = false
    // Add the AllowedCorOrigins to get the Access-Control-Allow-Origin header to be inserted for the following domains
    ,AllowedCorsOrigins = new List<string>
    {
        "http://localhost:62561/"
        ,"http://portaldev.RogueOne.com"
        ,"https://portaldev.RogueOne.com"
    }
}    

The AllowedCorsOrigins was the last piece of my puzzle. Hopefully this helps someone else who is facing the same issue