I have a .NET core 2.1 web API which uses Swashbuckle.AspNetCore v3.0.0 to generate the swagger documentation. I can access the swagger UI from the API which works perfectly and I don't receive any errors when using it from the API. I cannot, however, use the swagger.json to publish this API to Azure API Management. When I load the swagger.json in the swagger editor for validation, it complains about a bunch of errors (same errors that Azure complains about). Since the swagger.json is generated from the swagger configured in my API (it's a link right at the top of the swagger UI within my API), why can't it be successfully loaded into the swagger editor or to Azure?
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = settings.AppSettings.ApplicationName + " API", Version = "v1" });
var commentPath = String.Format(#"{0}\{1}.xml", AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName);
c.IncludeXmlComments(commentPath);
});
{"swagger":"2.0","info":{"version":"v1","title":"Route Manager API"},"paths":{"/api/account/Register":{"post":{"tags":["Account"],"summary":"Allows for registering a customer or vendor.","operationId":"ApiAccountRegisterPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"UserName","in":"query","description":"Unique name of user being registered.","required":true,"type":"string"},{"name":"Password","in":"query","description":"Password for new user.","required":true,"type":"string"},{"name":"ConfirmPassword","in":"query","description":"Confirmation of password that must match.","required":true,"type":"string"},{"name":"Email","in":"query","description":"Email address of registring user.","required":true,"type":"string"}],"responses":{"200":{"description":"Successfully registered customer or vendor.","schema":{"$ref":"#/definitions/IdentityResult"}},"400":{"description":"The RegisterUserDto model was invalid.","schema":{"$ref":"#/definitions/RegisterUserDto"}}}}},"/api/account/ChangePassword":{"post":{"tags":["Account"],"summary":"Allows for changing the users password.","operationId":"ApiAccountChangePasswordPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"OldPassword","in":"query","required":true,"type":"string","format":"password"},{"name":"NewPassword","in":"query","required":true,"type":"string","format":"password","maxLength":100,"minLength":6},{"name":"ConfirmPassword","in":"query","required":true,"type":"string","format":"password"},{"name":"StatusMessage","in":"query","required":false,"type":"string"}],"responses":{"400":{"description":"The ChangePasswordDto model was invalid.","schema":{"$ref":"#/definitions/ChangePasswordDto"}},"200":{"description":"Password successfully changed"}}}},"/api/account/Logout":{"post":{"tags":["Account"],"summary":"Logs the current user out of the system.","operationId":"ApiAccountLogoutPost","consumes":[],"produces":[],"parameters":[],"responses":{"200":{"description":"Logout successfully performed"}}}},"/api/account/ExternalLogin":{"get":{"tags":["Account"],"summary":"Initiates the external login process for the specified authentication provider.","description":"When an external login is not already associated with a local account, an attempt is made to find one local account with the same email \r\naddress. If one is found, an account is created and associated with the external login. If an account is not found, this method will \r\nreturn the external login email to the caller so they can decide if they want to present an option for the user to provide an \r\nexisting email account or create a new one based off the external login email. If we always created a new account with the external \r\nemail then that would mean we could only associate external logins that have the same email address of an account stored in our system\r\nwhich would essentially allow duplicate accounts to be created for the same physical person.","operationId":"ApiAccountExternalLoginGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"provider","in":"query","description":"The code for the external login provider that is being used to login.","required":false,"type":"string"},{"name":"returnUrl","in":"query","description":"The return URL that will be called upon a successfully external login.","required":false,"type":"string"}],"responses":{"400":{"description":"Error occured during the external login process. Response should contain error description.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"200":{"description":"Indicate a token was issued and the user has been signed in."},"202":{"description":"Indicates the user was successfully signed in but an account does not exist in our system in which case the email \r\n of the external login is returned in a ExternalLoginModel that should be used to call the ExternalLoginConfirmation action method."}}}},"/api/account/ExternalLoginCallback":{"get":{"tags":["Account"],"summary":"Attempts to sign in a user based on an external sign-in attempt.","description":"This action is called from an external login provider after a client calls ExternalLogin. The results of this call are \r\ndocumented in the ExternalLogin method since the client never calls this method directly.","operationId":"ApiAccountExternalLoginCallbackGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"returnUrl","in":"query","required":false,"type":"string"},{"name":"remoteError","in":"query","required":false,"type":"string"}],"responses":{"202":{"description":"Indicates the user was successfully signed in but an account does not exist in our system in which case the email \r\n of the external login is returned in a ExternalLoginModel that should be used to call the ExternalLoginConfirmation action method.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"400":{"description":"Error occured during the external login process. Response should contain error description.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"200":{"description":"Indicate a token was issued and the user has been signed in."}}}},"/api/account/ExternalLoginConfirmation":{"post":{"tags":["Account"],"summary":"Associates an external login with an existing user or new user.","description":"In order to successfully associate an email for an existing user to an external credentials such as \r\n facebook, the user must already be authenticated via the external login provider.","operationId":"ApiAccountExternalLoginConfirmationPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"Email","in":"query","description":"The external email address associated with the external provider login.","required":true,"type":"string"},{"name":"LoginProvider","in":"query","description":"The external provider that was used to authenticate.","required":false,"type":"string"},{"name":"ReturnUrl","in":"query","description":"The return URL that was provided by the client application when requesting an external login request.","required":false,"type":"string"}],"responses":{"400":{"description":"The model was found to be invalid. Response should contain model errors.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"200":{"description":"Successfully associates the account to the external login and redirects to the client return URL."}}}},"/connect/authorize":{"get":{"tags":["Authorization"],"summary":"Provides the authorization endpoint for supporting openid connect code flow","operationId":"ConnectAuthorizeGet","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"AccessToken","in":"query","required":false,"type":"string"},{"name":"AcrValues","in":"query","required":false,"type":"string"},{"name":"Assertion","in":"query","required":false,"type":"string"},{"name":"Claims","in":"query","required":false,"type":"object"},{"name":"ClaimsLocales","in":"query","required":false,"type":"string"},{"name":"ClientAssertion","in":"query","required":false,"type":"string"},{"name":"ClientAssertionType","in":"query","required":false,"type":"string"},{"name":"ClientId","in":"query","required":false,"type":"string"},{"name":"ClientSecret","in":"query","required":false,"type":"string"},{"name":"Code","in":"query","required":false,"type":"string"},{"name":"CodeChallenge","in":"query","required":false,"type":"string"},{"name":"CodeChallengeMethod","in":"query","required":false,"type":"string"},{"name":"CodeVerifier","in":"query","required":false,"type":"string"},{"name":"Display","in":"query","required":false,"type":"string"},{"name":"GrantType","in":"query","required":false,"type":"string"},{"name":"IdentityProvider","in":"query","required":false,"type":"string"},{"name":"IdTokenHint","in":"query","required":false,"type":"string"},{"name":"LoginHint","in":"query","required":false,"type":"string"},{"name":"MaxAge","in":"query","required":false,"type":"integer","format":"int64"},{"name":"Nonce","in":"query","required":false,"type":"string"},{"name":"Password","in":"query","required":false,"type":"string"},{"name":"PostLogoutRedirectUri","in":"query","required":false,"type":"string"},{"name":"Prompt","in":"query","required":false,"type":"string"},{"name":"RedirectUri","in":"query","required":false,"type":"string"},{"name":"RefreshToken","in":"query","required":false,"type":"string"},{"name":"Request","in":"query","required":false,"type":"string"},{"name":"RequestId","in":"query","required":false,"type":"string"},{"name":"RequestUri","in":"query","required":false,"type":"string"},{"name":"Resource","in":"query","required":false,"type":"string"},{"name":"ResponseMode","in":"query","required":false,"type":"string"},{"name":"ResponseType","in":"query","required":false,"type":"string"},{"name":"Scope","in":"query","required":false,"type":"string"},{"name":"State","in":"query","required":false,"type":"string"},{"name":"Token","in":"query","required":false,"type":"string"},{"name":"TokenTypeHint","in":"query","required":false,"type":"string"},{"name":"Registration","in":"query","required":false,"type":"object"},{"name":"UiLocales","in":"query","required":false,"type":"string"},{"name":"Username","in":"query","required":false,"type":"string"}],"responses":{"200":{"description":"Successfully authenticated via the authorize flow and returns the appropriate access/identity tokens.","schema":{"$ref":"#/definitions/SignInResult"}},"400":{"description":"Indicates the user was authenticated, but the user manager was unable to obtain the user due to a server error."},"403":{"description":"Indicates the authorization request is not allowed because the user is not authenticated."}}}},"/connect/token":{"post":{"tags":["Authorization"],"summary":"Provides the token endpoint for supporting openidconnect token generation.","operationId":"ConnectTokenPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"AccessToken","in":"query","required":false,"type":"string"},{"name":"AcrValues","in":"query","required":false,"type":"string"},{"name":"Assertion","in":"query","required":false,"type":"string"},{"name":"Claims","in":"query","required":false,"type":"object"},{"name":"ClaimsLocales","in":"query","required":false,"type":"string"},{"name":"ClientAssertion","in":"query","required":false,"type":"string"},{"name":"ClientAssertionType","in":"query","required":false,"type":"string"},{"name":"ClientId","in":"query","required":false,"type":"string"},{"name":"ClientSecret","in":"query","required":false,"type":"string"},{"name":"Code","in":"query","required":false,"type":"string"},{"name":"CodeChallenge","in":"query","required":false,"type":"string"},{"name":"CodeChallengeMethod","in":"query","required":false,"type":"string"},{"name":"CodeVerifier","in":"query","required":false,"type":"string"},{"name":"Display","in":"query","required":false,"type":"string"},{"name":"GrantType","in":"query","required":false,"type":"string"},{"name":"IdentityProvider","in":"query","required":false,"type":"string"},{"name":"IdTokenHint","in":"query","required":false,"type":"string"},{"name":"LoginHint","in":"query","required":false,"type":"string"},{"name":"MaxAge","in":"query","required":false,"type":"integer","format":"int64"},{"name":"Nonce","in":"query","required":false,"type":"string"},{"name":"Password","in":"query","required":false,"type":"string"},{"name":"PostLogoutRedirectUri","in":"query","required":false,"type":"string"},{"name":"Prompt","in":"query","required":false,"type":"string"},{"name":"RedirectUri","in":"query","required":false,"type":"string"},{"name":"RefreshToken","in":"query","required":false,"type":"string"},{"name":"Request","in":"query","required":false,"type":"string"},{"name":"RequestId","in":"query","required":false,"type":"string"},{"name":"RequestUri","in":"query","required":false,"type":"string"},{"name":"Resource","in":"query","required":false,"type":"string"},{"name":"ResponseMode","in":"query","required":false,"type":"string"},{"name":"ResponseType","in":"query","required":false,"type":"string"},{"name":"Scope","in":"query","required":false,"type":"string"},{"name":"State","in":"query","required":false,"type":"string"},{"name":"Token","in":"query","required":false,"type":"string"},{"name":"TokenTypeHint","in":"query","required":false,"type":"string"},{"name":"Registration","in":"query","required":false,"type":"object"},{"name":"UiLocales","in":"query","required":false,"type":"string"},{"name":"Username","in":"query","required":false,"type":"string"}],"responses":{"200":{"description":"Successfully generates the appropriate access/identity tokens.","schema":{"$ref":"#/definitions/SignInResult"}},"400":{"description":"Indicates an error while attempting to generate an authentication token.","schema":{"$ref":"#/definitions/OpenIdConnectResponse"}},"403":{"description":"Indicates the authorization request is not allowed because the user is not authenticated."}}}},"/api/EntityActions":{"get":{"tags":["EntityAction"],"summary":"List endpoints for all enabled entity actions.","operationId":"ApiEntityActionsGet","consumes":[],"produces":[],"parameters":[],"responses":{"200":{"description":"Success"}}}},"/api/Environments":{"get":{"tags":["Environments"],"summary":"Returns all available environments.","description":"This method return all public environments. If the caller has been authenticated, additional environments configured\r\nfor the identity will also be returned.","operationId":"ApiEnvironmentsGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[],"responses":{"200":{"description":"Environments successfully returned","schema":{"uniqueItems":false,"type":"array","items":{"$ref":"#/definitions/ApplicationEnvironmentDto"}}},"500":{"description":"Unexpected error while retrieving available environments"}}}},"/api/Environments/{clientID}":{"get":{"tags":["Environments"],"summary":"Returns the set of environment configured for a given clientID","description":"This method return all public environments. If the caller has been authenticated, additional environments configured\r\nfor the identity will also be returned.","operationId":"ApiEnvironmentsByClientIDGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"clientID","in":"path","description":"The clientID assigned to the application requesting environments.","required":true,"type":"string"}],"responses":{"200":{"description":"Environments successfully returned","schema":{"uniqueItems":false,"type":"array","items":{"$ref":"#/definitions/ApplicationEnvironmentDto"}}},"400":{"description":"The client ID was not provided."},"500":{"description":"Unexpected error while retrieving available environments"}}}}},"definitions":{"IdentityResult":{"type":"object","properties":{"succeeded":{"type":"boolean","readOnly":true},"errors":{"uniqueItems":false,"type":"array","items":{"$ref":"#/definitions/IdentityError"},"readOnly":true}}},"IdentityError":{"type":"object","properties":{"code":{"type":"string"},"description":{"type":"string"}}},"RegisterUserDto":{"required":["userName","password","confirmPassword","email"],"type":"object","properties":{"userName":{"description":"Unique name of user being registered.","type":"string"},"password":{"description":"Password for new user.","type":"string"},"confirmPassword":{"description":"Confirmation of password that must match.","type":"string"},"email":{"description":"Email address of registring user.","type":"string"}}},"ChangePasswordDto":{"required":["oldPassword","newPassword","confirmPassword"],"type":"object","properties":{"oldPassword":{"format":"password","type":"string"},"newPassword":{"format":"password","maxLength":100,"minLength":6,"type":"string"},"confirmPassword":{"format":"password","type":"string"},"statusMessage":{"type":"string"}}},"ExternalLoginConfirmationDto":{"required":["email"],"type":"object","properties":{"email":{"description":"The external email address associated with the external provider login.","type":"string"},"loginProvider":{"description":"The external provider that was used to authenticate.","type":"string"},"returnUrl":{"description":"The return URL that was provided by the client application when requesting an external login request.","type":"string"}}},"SignInResult":{"type":"object","properties":{"succeeded":{"type":"boolean","readOnly":true},"isLockedOut":{"type":"boolean","readOnly":true},"isNotAllowed":{"type":"boolean","readOnly":true},"requiresTwoFactor":{"type":"boolean","readOnly":true}}},"OpenIdConnectResponse":{"type":"object","properties":{"accessToken":{"type":"string"},"code":{"type":"string"},"error":{"type":"string"},"errorDescription":{"type":"string"},"errorUri":{"type":"string"},"expiresIn":{"format":"int64","type":"integer"},"idToken":{"type":"string"},"refreshToken":{"type":"string"},"resource":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"tokenType":{"type":"string"}}},"ApplicationEnvironmentDto":{"type":"object","properties":{"applicationDisplayName":{"type":"string"},"environmentName":{"type":"string"},"baseUrl":{"type":"string"}}}}}
Here is an example function where the swagger is breaking down the properties of the Openiddict type within this function.
[HttpPost("~/connect/token")]
[ProducesResponseType(typeof(Microsoft.AspNetCore.Identity.SignInResult), 200)]
[ProducesResponseType(typeof(OpenIdConnectResponse), 400)]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
return Ok(null);
}
That function generates the following swagger.json from swashbuckle
/connect/token:
post:
tags:
- Authorization
summary: Provides the token endpoint for supporting openidconnect token generation.
operationId: ConnectTokenPost
consumes: []
produces:
- text/plain
- application/json
- text/json
- application/xml
- text/xml
parameters:
- name: AccessToken
in: query
required: false
type: string
- name: AcrValues
in: query
required: false
type: string
- name: Assertion
in: query
required: false
type: string
- name: Claims
in: query
required: false
type: object
- name: ClaimsLocales
in: query
required: false
type: string
- name: ClientAssertion
in: query
required: false
type: string
- name: ClientAssertionType
in: query
required: false
type: string
- name: ClientId
in: query
required: false
type: string
- name: ClientSecret
in: query
required: false
type: string
- name: Code
in: query
required: false
type: string
- name: CodeChallenge
in: query
required: false
type: string
- name: CodeChallengeMethod
in: query
required: false
type: string
- name: CodeVerifier
in: query
required: false
type: string
- name: Display
in: query
required: false
type: string
- name: GrantType
in: query
required: false
type: string
- name: IdentityProvider
in: query
required: false
type: string
- name: IdTokenHint
in: query
required: false
type: string
- name: LoginHint
in: query
required: false
type: string
- name: MaxAge
in: query
required: false
type: integer
format: int64
- name: Nonce
in: query
required: false
type: string
- name: Password
in: query
required: false
type: string
- name: PostLogoutRedirectUri
in: query
required: false
type: string
- name: Prompt
in: query
required: false
type: string
- name: RedirectUri
in: query
required: false
type: string
- name: RefreshToken
in: query
required: false
type: string
- name: Request
in: query
required: false
type: string
- name: RequestId
in: query
required: false
type: string
- name: RequestUri
in: query
required: false
type: string
- name: Resource
in: query
required: false
type: string
- name: ResponseMode
in: query
required: false
type: string
- name: ResponseType
in: query
required: false
type: string
- name: Scope
in: query
required: false
type: string
- name: State
in: query
required: false
type: string
- name: Token
in: query
required: false
type: string
- name: TokenTypeHint
in: query
required: false
type: string
- name: Registration
in: query
required: false
type: object
- name: UiLocales
in: query
required: false
type: string
- name: Username
in: query
required: false
type: string
responses:
'200':
description: Successfully generates the appropriate access/identity tokens.
schema:
$ref: '#/definitions/SignInResult'
'400':
description: Indicates an error while attempting to generate an authentication token.
schema:
$ref: '#/definitions/OpenIdConnectResponse'
'403':
description: Indicates the authorization request is not allowed because the user is not authenticated.
[HttpGet("~/connect/authorize")]
[ProducesResponseType(typeof(Microsoft.AspNetCore.Identity.SignInResult), 200)]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
return Ok(null);
}
Here is what I can tell you from the code you shared:
{
"name": "Claims",
"in": "query",
"required": false,
"type": "object"
}
That does not follow the Open Api Specification:
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#fixed-fields-7
If in is any value other than "body"...
Since the parameter is not located at the request body, it is limited to simple types (that is, not an object). The value MUST be one of "string", "number", "integer", "boolean", "array" or "file".
If you manually change those "type": "object" to "type": "string" in the editor the errors will go away.
Change your action to include FromBody:
public async Task<IActionResult> Exchange([FromBody] OpenIdConnectRequest request)
Somehow swashbuckle is getting confused and thinking that is a query param when that should be in the body