Merge pull request #71 from domaindrivendev/basic-auth-take2

Basic auth take2
This commit is contained in:
domaindrivendev 2017-06-28 00:16:38 -07:00 committed by GitHub
commit 519eb74cdd
11 changed files with 209 additions and 34 deletions

View File

@ -81,24 +81,24 @@ GEM
railties (>= 3.0.0)
globalid (0.4.0)
activesupport (>= 4.2.0)
i18n (0.8.1)
i18n (0.8.4)
json (1.8.6)
json-schema (2.8.0)
addressable (>= 2.4)
libv8 (3.16.14.15)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.5)
mail (2.6.6)
mime-types (>= 1.16, < 4)
method_source (0.8.2)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
mini_portile2 (2.2.0)
minitest (5.10.2)
nio4r (2.0.0)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
power_assert (0.3.1)
rack (2.0.3)
rack-test (0.6.3)

View File

@ -200,6 +200,58 @@ describe 'Blogs API', swagger_doc: 'v2/swagger.json' do
end
```
### Specifying/Testing API Security ###
Swagger allows for the specification of different security schemes and their applicability to operations in an API. To leverage this in rswag, you define the schemes globally in _swagger_helper.rb_ and then use the "security" attribute at the operation level to specify which schemes, if any, are applicable to that operation. Swagger supports :basic, :apiKey and :oauth2 scheme types. See [the spec](http://swagger.io/specification/#security-definitions-object-109) for more info.
```ruby
# spec/swagger_helper.rb
RSpec.configure do |config|
config.swagger_root = Rails.root.to_s + '/swagger'
config.swagger_docs = {
'v1/swagger.json' => {
...
securityDefinitions: {
basic: {
type: :basic
},
apiKey: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
}
end
# spec/integration/blogs_spec.rb
describe 'Blogs API' do
path '/blogs' do
post 'Creates a blog' do
tags 'Blogs'
security [ basic: [] ]
...
response '201', 'blog created' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
run_test!
end
response '401', 'authentication failed' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('bogus:bogus')}" }
run_test!
end
end
end
end
```
__NOTE:__ Depending on the scheme types, you'll be required to assign a corresponding parameter value with each example. For example, :basic auth is required above and so the :Authorization (header) parameter must be set accordingly
## Configuration & Customization ##
The steps described above will get you up and running with minimal setup. However, rswag offers a lot of flexibility to customize as you see fit. Before exploring the various options, you'll need to be aware of it's different components. The following table lists each of them and the files that get added/updated as part of a standard install.

View File

@ -14,7 +14,7 @@ module Rswag
api_metadata[:operation][:verb],
factory.build_fullpath(self),
factory.build_body(self),
factory.build_headers(self)
rackify_headers(factory.build_headers(self)) # Rails test infrastructure requires Rack headers
)
else
send(
@ -36,6 +36,22 @@ module Rswag
private
def rackify_headers(headers)
name_value_pairs = headers.map do |name, value|
[
case name
when 'Accept' then 'HTTP_ACCEPT'
when 'Content-Type' then 'CONTENT_TYPE'
when 'Authorization' then 'HTTP_AUTHORIZATION'
else name
end,
value
]
end
Hash[ name_value_pairs ]
end
def rswag_config
::Rswag::Specs.config
end

View File

@ -32,13 +32,20 @@ module Rswag
end
def build_headers(example)
headers = Hash[ parameters_in(:header).map { |p| [ p[:name], example.send(p[:name]).to_s ] } ]
headers.tap do |h|
name_value_pairs = parameters_in(:header).map do |param|
[
param[:name],
example.send(param[:name]).to_s
]
end
# Add MIME type headers based on produces/consumes metadata
produces = @api_metadata[:operation][:produces] || @global_metadata[:produces]
consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes]
h['ACCEPT'] = produces.join(';') unless produces.nil?
h['CONTENT_TYPE'] = consumes.join(';') unless consumes.nil?
end
name_value_pairs << [ 'Accept', produces.join(';') ] unless produces.nil?
name_value_pairs << [ 'Content-Type', consumes.join(';') ] unless consumes.nil?
Hash[ name_value_pairs ]
end
private
@ -52,7 +59,7 @@ module Rswag
applicable_params
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references
.concat(resolve_api_key_parameters)
.concat(security_parameters)
.select { |p| p[:in] == location }
end
@ -63,17 +70,23 @@ module Rswag
defined_params[key]
end
def resolve_api_key_parameters
@api_key_params ||= begin
def security_parameters
applicable_security_schemes.map do |scheme|
if scheme[:type] == :apiKey
{ name: scheme[:name], type: :string, in: scheme[:in] }
else
{ name: 'Authorization', type: :string, in: :header } # use auth header for basic & oauth2
end
end
end
def applicable_security_schemes
# First figure out the security requirement applicable to the operation
global_requirements = (@global_metadata[:security] || [] ).map { |r| r.keys.first }
operation_requirements = (@api_metadata[:operation][:security] || [] ).map { |r| r.keys.first }
requirements = global_requirements | operation_requirements
requirements = @api_metadata[:operation][:security] || @global_metadata[:security]
scheme_names = requirements ? requirements.map { |r| r.keys.first } : []
# Then obtain the scheme definitions for those requirements
definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements)
definitions.values.select { |d| d[:type] == :apiKey }
end
(@global_metadata[:securityDefinitions] || {}).slice(*scheme_names).values
end
def build_query_string_part(param, value)

View File

@ -193,6 +193,33 @@ module Rswag
end
end
context "global definition for 'basic auth'" do
before do
global_metadata[:securityDefinitions] = { basic_auth: { type: :basic} }
allow(example).to receive(:'Authorization').and_return('Basic foobar')
end
context 'global requirement' do
before { global_metadata[:security] = [ { basic_auth: [] } ] }
it "includes a corresponding Authorization header" do
expect(headers).to match(
'Authorization' => 'Basic foobar'
)
end
end
context 'operation-specific requirement' do
before { api_metadata[:operation][:security] = [ { basic_auth: [] } ] }
it "includes a corresponding Authorization header" do
expect(headers).to match(
'Authorization' => 'Basic foobar'
)
end
end
end
context 'consumes & produces' do
before do
api_metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ]
@ -201,8 +228,8 @@ module Rswag
it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match(
'ACCEPT' => 'application/json;application/xml',
'CONTENT_TYPE' => 'application/json;application/xml'
'Accept' => 'application/json;application/xml',
'Content-Type' => 'application/json;application/xml'
)
end
end
@ -217,8 +244,8 @@ module Rswag
it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match(
'ACCEPT' => 'application/json;application/xml',
'CONTENT_TYPE' => 'application/json;application/xml'
'Accept' => 'application/json;application/xml',
'Content-Type' => 'application/json;application/xml'
)
end
end

View File

@ -1,7 +1,7 @@
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
protect_from_forgery with: :null_session
wrap_parameters format: [ :json ]
end

View File

@ -0,0 +1,13 @@
class AuthTestsController < ApplicationController
wrap_parameters Blog
respond_to :json
# POST /auth-tests/basic
def basic
if authenticate_with_http_basic { |u, p| u == 'jsmith' && p == 'jspass' }
head :no_content
else
request_http_basic_authentication
end
end
end

View File

@ -1,6 +1,8 @@
TestApp::Application.routes.draw do
resources :blogs, defaults: { :format => :json }
post 'auth-tests/basic', to: 'auth_tests#basic'
mount Rswag::Api::Engine => 'api-docs'
mount Rswag::Ui::Engine => 'api-docs'
end

View File

@ -0,0 +1,22 @@
require 'swagger_helper'
describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do
path '/auth-tests/basic' do
post 'Authenticates with basic auth' do
tags 'Auth Test'
operationId 'testBasicAuth'
security [ basic_auth: [] ]
response '204', 'Valid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
run_test!
end
response '401', 'Invalid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('foo:bar')}" }
run_test!
end
end
end
end

View File

@ -45,6 +45,9 @@ RSpec.configure do |config|
}
},
securityDefinitions: {
basic_auth: {
type: :basic
},
api_key: {
type: :apiKey,
name: 'api_key',

View File

@ -5,6 +5,30 @@
"version": "v1"
},
"paths": {
"/auth-tests/basic": {
"post": {
"summary": "Authenticates with basic auth",
"tags": [
"Auth Test"
],
"operationId": "testBasicAuth",
"security": [
{
"basic_auth": [
]
}
],
"responses": {
"204": {
"description": "Valid credentials"
},
"401": {
"description": "Invalid credentials"
}
}
}
},
"/blogs": {
"post": {
"summary": "Creates a blog",
@ -158,6 +182,9 @@
}
},
"securityDefinitions": {
"basic_auth": {
"type": "basic"
},
"api_key": {
"type": "apiKey",
"name": "api_key",