Browse Source

Implemented ApiAuthenticationController

Closes #24
feature/29
Martin Bober 4 months ago
parent
commit
7915047d65
  1. 1
      Gemfile
  2. 2
      Gemfile.lock
  3. 62
      app/controllers/api_authentication_controller.rb
  4. 1
      app/helpers/sessions_helper.rb
  5. 7
      config/application.rb
  6. 1
      config/routes.rb
  7. 4
      db/schema.rb
  8. 8
      openapi.yaml
  9. 119
      test/controllers/api_authentication_controller_test.rb

1
Gemfile

@ -29,6 +29,7 @@ gem 'puma', '~> 3.11'
gem 'redis', '~> 4.5', '>= 4.5.1'
gem 'listen', '~> 3.7'
gem 'sidekiq', '~> 6.3', '>= 6.3.1'
gem 'rack-cors', '~> 0.4.0'
group :development do
gem 'web-console', '~> 3.5'

2
Gemfile.lock

@ -157,6 +157,7 @@ GEM
method_source (~> 1.0)
puma (3.12.6)
rack (2.2.3)
rack-cors (0.4.1)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.6)
@ -278,6 +279,7 @@ DEPENDENCIES
paperclip (~> 5.0)
pg (= 0.17.1)
puma (~> 3.11)
rack-cors (~> 0.4.0)
rails (~> 5.0)
rails-controller-testing (~> 1.0, >= 1.0.1)
rails-i18n (~> 5.0.0)

62
app/controllers/api_authentication_controller.rb

@ -2,15 +2,73 @@ class ApiAuthenticationController < ApplicationController
before_action :check_mime
protect_from_forgery with: :null_session
def login
unless params[:response_type] == 'token'
resp = {
error: "unsupported_response_type",
error_description: "response_type must be set to 'token'.",
}
resp[:state] = params[:state] if params.key? :state
render status: :bad_request, json: resp
return
end
[:client_id, :user_name, :password].each do |p|
unless params.key? p
resp = {
error: "invalid_request",
error_description: "Request is missing '#{p}' parameter.",
}
resp[:state] = params[:state] if params.key? :state
render status: :bad_request, json: resp
return
end
end
sleep 2 # Wait for 2 seconds to slow down brute force attacks
player = Player.find_by(email: params[:user_name].downcase)
if player && player.authenticate(params[:password])
expires = 1.year
token = log_in player, false, expires
resp = {
access_token: token,
token_type: 'bearer',
expires_in: expires.seconds.to_i
}
resp[:state] = params[:state] if params.key? :state
render json: resp
else
resp = {
error: "access_denied",
error_description: "Invalid email/password combination",
}
resp[:state] = params[:state] if params.key? :state
render status: :bad_request, json: resp
end
end
def logout
unless logged_in?
render status: :bad_request, json: {
error: "invalid_token",
access_token: current_auth_token
}
return
end
AccessToken.where(token: current_auth_token).destroy_all
render json: {
access_token: current_auth_token
}
end
private
def check_mime
unless request.content_type == "application/json"
head 400
unless request.body.nil? || request.body.size == 0 || request.content_type == "application/json"
render status: :bad_request, json: {
error: "bad_content_type",
error_description: "Content-type header must be set to 'application/json'."
}
return true
end
end

1
app/helpers/sessions_helper.rb

@ -12,6 +12,7 @@ module SessionsHelper
else
cookies[:auth_token] = token
end
token
end
def current_auth_token

7
config/application.rb

@ -36,5 +36,12 @@ module Charxchange
end
end
config.middleware.insert_before ActionDispatch::Static, Rack::Cors do
allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :options]
end
end
end
end

1
config/routes.rb

@ -150,6 +150,7 @@ Rails.application.routes.draw do
scope '/api' do
scope '/1.0' do
post 'login', controller: :api_authentication, action: :login
post 'logout', controller: :api_authentication, action: :logout
end
end
end

4
db/schema.rb

@ -22,7 +22,7 @@ ActiveRecord::Schema.define(version: 2022_05_15_100551) do
t.index ["token"], name: "index_access_tokens_on_token"
end
create_table "achievements", options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
create_table "achievements", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t|
t.string "name"
t.text "description"
t.bigint "campaign_id"
@ -196,7 +196,7 @@ ActiveRecord::Schema.define(version: 2022_05_15_100551) do
t.index ["player_id"], name: "index_date_polls_on_player_id"
end
create_table "earned_achievements", options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
create_table "earned_achievements", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "achievement_id"
t.bigint "player_id"
t.bigint "campaign_session_id"

8
openapi.yaml

@ -20,9 +20,9 @@ paths:
post:
tags:
- "Authorization"
description: Generates a bearer token to be used in the `Authorization` header to authenticate the client.
description: Generates a bearer token to be used in the `Authorization` header to authenticate the client, similar to [OAuth 2.0 Implicit Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2)
parameters:
- name: responseType
- name: response_type
in: query
required: true
description: Must be set to `token`
@ -59,7 +59,7 @@ paths:
description: Player's password
responses:
200:
description: New token was issued
description: New token was issued. Response follows [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750#section-4)
content:
'application/json':
schema:
@ -68,9 +68,9 @@ paths:
access_token:
type: string
description: The bearer token you can use in the `Authorization` header to authenticate as this user
example: bearer
token_type:
type: string
example: bearer
expires_in:
type: integer
description: Number of seconds until the token expires

119
test/controllers/api_authentication_controller_test.rb

@ -6,11 +6,120 @@ class ApiAuthenticationControllerTest < ActionController::TestCase
@player = Player.first
end
test 'player cannot create token without response_type' do
state = 'state'
assert_no_difference 'AccessToken.count' do
post :login, as: :json, body: {
client_id: 'something',
user_name: @player.email,
password: 'password',
state: 'state'
}.to_json
assert_response :bad_request
end
body = JSON.parse response.body
assert_equal 'unsupported_response_type', body["error"]
assert_equal state, body["state"]
end
test 'player cannot create token with wrong response_type' do
state = 'state'
assert_no_difference 'AccessToken.count' do
post :login, as: :json, body: {
response_type: 'jwt',
client_id: 'something',
user_name: @player.email,
password: 'password',
state: 'state'
}.to_json
assert_response :bad_request
end
body = JSON.parse response.body
assert_equal 'unsupported_response_type', body["error"]
assert_equal state, body["state"]
end
test 'player cannot create token without client_id' do
state = 'state'
assert_no_difference 'AccessToken.count' do
post :login, as: :json, body: {
response_type: 'token',
user_name: @player.email,
password: 'password',
state: 'state'
}.to_json
assert_response :bad_request
end
body = JSON.parse response.body
assert_equal 'invalid_request', body["error"]
assert_equal state, body["state"]
end
test 'player cannot create token without password' do
state = 'state'
assert_no_difference 'AccessToken.count' do
post :login, as: :json, body: {
response_type: 'token',
client_id: 'something',
user_name: @player.email,
state: 'state'
}.to_json
assert_response :bad_request
end
body = JSON.parse response.body
assert_equal 'invalid_request', body["error"]
assert_equal state, body["state"]
end
test 'player cannot create token with incorrect credentials' do
state = 'state'
assert_no_difference 'AccessToken.count' do
start = Time.now
post :login, as: :json, body: {
response_type: 'token',
client_id: 'something',
user_name: @player.email,
password: 'passwordd',
state: 'state'
}.to_json
completion = Time.now
assert_response :bad_request
assert((completion - start) > 1.second)
end
body = JSON.parse response.body
assert_equal 'access_denied', body["error"]
assert_equal state, body["state"]
end
test 'player can create token with correct credentials' do
post '/api/1.0/login', body: {
user_name: @player.email,
password: 'password'
}
assert_response :success
state = 'state'
assert_difference 'AccessToken.count', 1 do
start = Time.now
post :login, as: :json, body: {
response_type: 'token',
client_id: 'something',
user_name: @player.email,
password: 'password',
state: 'state'
}.to_json
completion = Time.now
assert_response :success
assert((completion - start) > 1.second)
end
t = AccessToken.last
assert_equal @player, t.player
body = JSON.parse response.body
assert_equal 'bearer', body["token_type"]
assert_equal t.token, body["access_token"]
assert_equal state, body["state"]
assert_equal 1.year.seconds.to_i, body["expires_in"]
end
test 'player can revoke token' do
log_in_as @player
assert_difference 'AccessToken.count', -1 do
post :logout
end
end
end

Loading…
Cancel
Save