Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

formData will not validate schemas with oneOf #267

Closed
Tracked by #318
CameronGo opened this issue Nov 17, 2020 · 3 comments
Closed
Tracked by #318

formData will not validate schemas with oneOf #267

CameronGo opened this issue Nov 17, 2020 · 3 comments

Comments

@CameronGo
Copy link

If you are using a content type of application/x-www-form-urlencoded you cannot validate a request whose schema uses oneOf. The result says that the schema does not match oneOf. I've tried moving the oneOf schemas around to diff locations and the result is always the same. Here is a stripped down example of the api doc being used.

openapi: 3.0.0
info:
  description: This is a sample of the API
  version: '{version}'
  title: sample API
tags:
  - name: authorization
    description: Create and validate authorization tokens using oauth
paths:
  '/oauth2/token':
    post:
      tags:
      - authorization
      security:
      - basicAuth: []
      - {}
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AccessTokenRequest'
            examples:
              ClientCredentialsTokenRequest:
                value:
                  grant_type: client_credentials
                  scope: 'member:read member:write'
              PasswordTokenRequest:
                value:
                  grant_type: password
                  client_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6'
                  scope: 'member:read member:write'                  
                  username: '[email protected]'
                  password: '2R.kNmv@(H'
              DeviceCodeTokenRequest:
                value:
                  grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
                  client_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6'
                  device_code: '1d5baab516044f26a43550a2dc7ea8ae'
              RefreshTokenRequest:
                value:
                  grant_type: refresh_token
                  client_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6'
                  refresh_token: '2fbd6ad96acc4fa99ef36a3e803b010b'
      responses:
        '200':
          description: 'The request was successful and a token was issued.'
          headers:
            Cache-Control:
              description: 'This header must always be returned to ensure proper handling of the token.'
              schema:
                type: string
                enum:
                - 'no-store'
                example: 'no-store'
            Pragma: 
              description: 'This header must always be returned to ensure proper handling of the token.'
              schema:
                type: string
                enum:
                - 'no-cache'
                example: 'no-cache'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessToken'
              examples:
                ClientCredentialsToken:
                  $ref: '#/components/examples/ClientCredentialsToken'
                UserPasswordToken:
                  $ref: '#/components/examples/UserPasswordToken'
                DeviceToken:
                  $ref: '#/components/examples/DeviceToken'
                RefreshToken:
                  $ref: '#/components/examples/RefreshToken'  
        '400':
          description: 'Bad request. The request submitted is formatted incorrectly or contains bad data. Fix the request and try again.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthError'
        '401':
          description: 'The supplied credentials are invalid or have expired. Please fix the credentials and try again.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthError'
              example:
                error: invalid_client
        '403':
          description: 'The authenticated client is not authorized to use this authorization grant type.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthError'
              example:
                error: unauthorized_client                
        '500':
          $ref: '#/components/responses/500' 
         
components:
  responses:
    '500':
      description: >-
        There was an unexpected error or failure. You should wait a moment and
        retry your request.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic            
  schemas:
    ErrorMessage:
      description: >-
        The schema to which all error responses should conform with and
        explanation of the issue in the message attribute.
      type: object
      required:
        - message
      properties:
        message:
          type: string
          example: Error message
    OauthError:
      description: 'Please refer to oauth RFC on error responses: https://tools.ietf.org/html/rfc6749#section-5.2'
      type: object
      properties:
        error:
          type: string
          enum:
          - invalid_request
          - invalid_client
          - invalid_grant
          - unauthorized_client
          - unsupported_grant_type
          - invalid_scope
          - authorization_pending
          - access_denied
          - expired_token
        error_description:
          type: string
          example: the request did not specify a grant type
      example:
        error: 'invalid_request'
      required:
      - error

    AccessTokenRequest:
      description: 'Describes all of the potential access token requests that can be received'
      type: object
      oneOf:
      - $ref: '#/components/schemas/ClientCredentialsTokenRequest'
      - $ref: '#/components/schemas/PasswordTokenRequest'
      - $ref: '#/components/schemas/DeviceAccessTokenRequest'
      - $ref: '#/components/schemas/RefreshTokenRequest'
    ClientCredentialsTokenRequest:
      description: 'The client_id and client_secret properties should only be sent in form data if the client does not support basic authentication for sending client credentials.'
      properties:
        grant_type:
          type: string
          enum:
          - client_credentials
          example: 'client_credentials'
        scope:
          $ref: '#/components/schemas/AccessToken/properties/scope'      
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id'
        client_secret:
          description: 'A secret code that would be setup for the client to exchange for an access token.'
          type: string
          example: 'fac663c0-e8b5-4c02-9ad3-ddbd1bbc6964'
      required:
      - grant_type
      - scope
    PasswordTokenRequest:
      type: object
      properties:
        grant_type:
          type: string
          enum:
          - password
          example: 'password'
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id'
        username:
          description: 'The username for the user account on whose behalf the token will be used.'
          type: string
          example: '[email protected]'
        password:
          description: 'The password for the user account on whose behalf the token will be used.'
          type: string
          format: password
          example: '2R.kNmv@(H'
        scope:
          $ref: '#/components/schemas/AccessToken/properties/scope'          
      required:
      - grant_type
      - client_id
      - username
      - password
      - scope
    DeviceAccessTokenRequest:
      type: object
      properties:
        grant_type:
          type: string
          enum:
          - urn:ietf:params:oauth:grant-type:device_code
          example: urn:ietf:params:oauth:grant-type:device_code
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id'
        device_code:
          description: 'This is a long string that the device will use to eventually exchange for an access token'
          type: string
          minLength: 32
          example: '1d5baab516044f26a43550a2dc7ea8ae'
        scope:
          $ref: '#/components/schemas/AccessToken/properties/scope'          
      required:
      - grant_type
      - client_id
      - device_code
      - scope
    RefreshTokenRequest:
      type: object
      properties:
        grant_type:
          type: string
          enum:
          - refresh_token
          example: 'refresh_token'
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id'
        refresh_token:
          $ref: '#/components/schemas/AccessToken/properties/refresh_token'
      required:
      - grant_type
      - client_id
      - refresh_token
    AccessToken:
      type: object
      properties:
        access_token:
          description: 'The token which should be passed in the authorization header to gain access to the resource server'
          type: string
          minLength: 32
          example: '8551c99d47f847bebe718e483bfb5c65'
        token_type:
          description: 'All tokens for oauth will be of type Bearer.'
          type: string
          enum:
          - Bearer
          example: Bearer
        expires_in:
          description: 'The number of seconds in which the access token expires'
          type: integer
          example: 3600
        scope:
          description: 'A space separated list of scopes requested for the token'
          type: string
          example: 'member:read member:write'
        client_id:
          description: 'The ID provided when the client application was registered'
          type: string
          example: '3fa85f64-5717-4562-b3fc-2c963f66afa6'
        refresh_token:
          description: 'A long lived one time use token that is issued only in cases where the client can be offline or restarted and where the authorization should persist.'
          type: string
          minLength: 32
          example: '2fbd6ad96acc4fa99ef36a3e803b010b'
        user_id:
          description: 'Identifies the UUID of the account in the case of a token issued for user authentication. Will not be used in client credentials or device grant types. This property is being deprecated and the client application should not expect it to be present.'
          deprecated: true
          type: string
          format: uuid
          example: ''
      required:
      - access_token
      - token_type
      - scope
      - expires_in
      - client_id
  examples:
    ClientCredentialsToken:
      value:
        access_token:
          $ref: '#/components/schemas/AccessToken/properties/access_token/example'
        token_type:
          $ref: '#/components/schemas/AccessToken/properties/token_type/example'
        expires_in:
          $ref: '#/components/schemas/AccessToken/properties/expires_in/example'
        scope:
          $ref: '#/components/schemas/AccessToken/properties/scope/example'
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id/example'
    UserPasswordToken:
      value:
        access_token:
          $ref: '#/components/schemas/AccessToken/properties/access_token/example'
        token_type:
          $ref: '#/components/schemas/AccessToken/properties/token_type/example'
        expires_in:
          $ref: '#/components/schemas/AccessToken/properties/expires_in/example'
        scope:
          $ref: '#/components/schemas/AccessToken/properties/scope/example'
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id/example'
        user_id:
          $ref: '#/components/schemas/AccessToken/properties/user_id/example'
    DeviceToken:
      value:
        access_token:
          $ref: '#/components/schemas/AccessToken/properties/access_token/example'
        token_type:
          $ref: '#/components/schemas/AccessToken/properties/token_type/example'
        expires_in:
          $ref: '#/components/schemas/AccessToken/properties/expires_in/example'
        scope:
          $ref: '#/components/schemas/AccessToken/properties/scope/example'
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id/example'
        refresh_token:
          $ref: '#/components/schemas/AccessToken/properties/refresh_token/example'
    RefreshToken:
      value:
        access_token:
          $ref: '#/components/schemas/AccessToken/properties/access_token/example'
        token_type:
          $ref: '#/components/schemas/AccessToken/properties/token_type/example'
        expires_in:
          $ref: '#/components/schemas/AccessToken/properties/expires_in/example'
        scope:
          $ref: '#/components/schemas/AccessToken/properties/scope/example'
        client_id:
          $ref: '#/components/schemas/AccessToken/properties/client_id/example'
        refresh_token:
          $ref: '#/components/schemas/AccessToken/properties/refresh_token/example'          
@fenollp
Copy link
Collaborator

fenollp commented Nov 17, 2020

Please provide your Go code as well. The easiest it is to reproduce, the fastest it can get fixed.

@mmixon808
Copy link

Attached below is an example that results in a log message of Request body has an error: doesn't match the schema: Doesn't match schema "oneOf" It appears that the urlencodedBodyDecoder doesn't look at the oneOf contents. It is only looking for a properties definition.

Attached is a bit of Go code with the spec doc as a constant to replicate the issue. Hope this helps. Let me know if you need anything else.

FYI, this library has saved us so much time in building our API's, and has really contributed to making them significantly more robust. We appreciate all the effort here.

package main

import (
	"context"
	"log"
	"net/http"
	"net/url"
	"strings"

	"github.com/getkin/kin-openapi/openapi3"
	"github.com/getkin/kin-openapi/openapi3filter"
)

func main() {

	router := openapi3filter.NewRouter()
	swagger, _ := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(spec))
	router.AddSwagger(swagger)

	routeURL, _ := url.Parse("http://localhost/oauth2/token")
	route, pathParams, _ := router.FindRoute("POST", routeURL)

	req, _ := http.NewRequest(
		"POST",
		"/oauth2/token",
		strings.NewReader("grant_type=client_credentials&scope=testscope&client_id=myclient&client_secret=mypass"))
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

	validationInput := &openapi3filter.RequestValidationInput{
		Request:    req,
		PathParams: pathParams,
		Route:      route,
	}

	if err := openapi3filter.ValidateRequest(context.TODO(), validationInput); err != nil {
		log.Fatalf("error in schema: %s", err)
	}
}

const spec = `openapi: 3.0.0
info:
  description: This is a sample of the API
  version: '{version}'
  title: sample API
tags:
  - name: authorization
    description: Create and validate authorization tokens using oauth
paths:
  '/oauth2/token':
    post:
      tags:
      - authorization
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AccessTokenRequest'
            examples:
              ClientCredentialsTokenRequest:
                value:
                  grant_type: client_credentials
                  scope: 'member:read member:write'
              RefreshTokenRequest:
                value:
                  grant_type: refresh_token
                  client_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6'
                  refresh_token: '2fbd6ad96acc4fa99ef36a3e803b010b'
      responses:
        '200':
          description: 'The request was successful and a token was issued.'

components:
  schemas:
    AccessTokenRequest:
      description: 'Describes all of the potential access token requests that can be received'
      type: object
      oneOf:
      - $ref: '#/components/schemas/ClientCredentialsTokenRequest'
      - $ref: '#/components/schemas/RefreshTokenRequest'
    ClientCredentialsTokenRequest:
      description: 'The client_id and client_secret properties should only be sent in form data if the client does not support basic authentication for sending client credentials.'
      properties:
        grant_type:
          type: string
          enum:
          - client_credentials
          example: 'client_credentials'
        scope:
          description: 'A space separated list of scopes requested for the token'
          type: string
          example: 'member:read member:write'
        client_id:
          description: 'The ID provided when the client application was registered'
          type: string
          example: '3fa85f64-5717-4562-b3fc-2c963f66afa6'
        client_secret:
          description: 'A secret code that would be setup for the client to exchange for an access token.'
          type: string
          example: 'fac663c0-e8b5-4c02-9ad3-ddbd1bbc6964'
      required:
      - grant_type
      - scope
    RefreshTokenRequest:
      type: object
      properties:
        grant_type:
          type: string
          enum:
          - refresh_token
          example: 'refresh_token'
        client_id:
          description: 'The ID provided when the client application was registered'
          type: string
          example: '3fa85f64-5717-4562-b3fc-2c963f66afa6'
        refresh_token:
          description: 'A long lived one time use token that is issued only in cases where the client can be offline or restarted and where the authorization should persist.'
          type: string
          minLength: 32
          example: '2fbd6ad96acc4fa99ef36a3e803b010b'
      required:
      - grant_type
      - client_id
      - refresh_token`

fenollp added a commit to fenollp/kin-openapi that referenced this issue Dec 19, 2022
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Dec 19, 2022
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Oct 20, 2023
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Nov 25, 2023
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Nov 26, 2023
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Nov 26, 2023
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Nov 26, 2023
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Nov 26, 2023
Signed-off-by: Pierre Fenoll <[email protected]>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Nov 26, 2023
Signed-off-by: Pierre Fenoll <[email protected]>
imtaketa pushed a commit to imtaketa/kin-openapi that referenced this issue Jan 25, 2024
imtaketa pushed a commit to imtaketa/kin-openapi that referenced this issue Jan 31, 2024
imtaketa pushed a commit to imtaketa/kin-openapi that referenced this issue Jan 31, 2024
imtaketa pushed a commit to imtaketa/kin-openapi that referenced this issue Feb 2, 2024
imtaketa pushed a commit to imtaketa/kin-openapi that referenced this issue Feb 2, 2024
@fenollp
Copy link
Collaborator

fenollp commented Feb 2, 2024

Fixed by #903

@fenollp fenollp closed this as completed Feb 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants