Commit 0892b7e

mo <mo.khan@gmail.com>
2018-10-21 22:41:46
move oauth code to oauth namespace.
1 parent 73ac04f
app/controllers/oauth/authorizations_controller.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Oauth
+  class AuthorizationsController < ApplicationController
+    VALID_RESPONSE_TYPES = %w[code token].freeze
+
+    def show
+      @client = Client.find(secure_params[:client_id])
+
+      unless @client.valid_redirect_uri?(secure_params[:redirect_uri])
+        return redirect_to @client.redirect_url(
+          error: :invalid_request,
+          state: secure_params[:state]
+        )
+      end
+
+      unless @client.valid_response_type?(secure_params[:response_type])
+        return redirect_to @client.redirect_url(
+          error: :unsupported_response_type,
+          state: secure_params[:state]
+        )
+      end
+
+      session[:oauth] = secure_params.to_h
+    end
+
+    def create(oauth = session[:oauth])
+      return render_error(:bad_request) if oauth.nil?
+
+      client = Client.find(oauth[:client_id])
+      redirect_to client.redirect_url_for(current_user, oauth)
+    rescue StandardError => error
+      logger.error(error)
+      redirect_to client.redirect_url(error: :invalid_request)
+    end
+
+    private
+
+    def secure_params
+      params.permit(
+        :client_id, :response_type, :redirect_uri,
+        :state, :code_challenge, :code_challenge_method
+      )
+    end
+  end
+end
app/controllers/oauth/clients_controller.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Oauth
+  class ClientsController < ApplicationController
+    skip_before_action :authenticate!
+    before_action :apply_cache_headers
+
+    def create
+      @client = Client.create!(transform(secure_params))
+      render status: :created, formats: :json
+    rescue ActiveRecord::RecordInvalid => error
+      json = {
+        error: error_type_for(error.record.errors),
+        error_description: error.record.errors.full_messages.join(' ')
+      }
+      render json: json, status: :bad_request
+    end
+
+    private
+
+    def secure_params
+      params.permit(
+        :client_name,
+        :token_endpoint_auth_method,
+        :logo_uri,
+        :jwks_uri,
+        redirect_uris: []
+      )
+    end
+
+    def transform(params)
+      {
+        name: params[:client_name],
+        redirect_uris: params[:redirect_uris],
+        token_endpoint_auth_method: params[:token_endpoint_auth_method],
+        logo_uri: params[:logo_uri],
+        jwks_uri: params[:jwks_uri],
+      }
+    end
+
+    def apply_cache_headers
+      response.headers["Cache-Control"] = "no-cache, no-store"
+      response.headers["Pragma"] = "no-cache"
+    end
+
+    def error_type_for(errors)
+      if errors[:redirect_uris].present?
+        :invalid_redirect_uri
+      else
+        :invalid_client_metadata
+      end
+    end
+  end
+end
app/controllers/oauth/tokens_controller.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Oauth
+  class TokensController < ApplicationController
+    def create
+      response.headers['Cache-Control'] = 'no-store'
+      response.headers['Pragma'] = 'no-cache'
+
+      @access_token, @refresh_token = tokens_for(params[:grant_type])
+      return bad_request if @access_token.nil?
+
+      render formats: :json
+    rescue StandardError => error
+      Rails.logger.error(error)
+      bad_request
+    end
+
+    def introspect
+      claims = Token.claims_for(params[:token], token_type: :any)
+      if claims.empty? || revoked_tokens[claims[:jti]]
+        render json: { active: false }, status: :ok
+      else
+        render json: claims.merge(active: true), status: :ok
+      end
+    end
+
+    def revoke
+      claims = Token.claims_for(params[:token], token_type: :any)
+      Token.find(claims[:jti]).revoke! unless claims.empty?
+      render plain: "", status: :ok
+    rescue StandardError => error
+      logger.error(error)
+      render plain: "", status: :ok
+    end
+
+    private
+
+    attr_reader :current_client
+
+    def authenticate!
+      @current_client = authenticate_with_http_basic do |client_id, client_secret|
+        Client.find(client_id)&.authenticate(client_secret)
+      end
+      return if current_client
+
+      render "invalid_client", formats: :json, status: :unauthorized
+    end
+
+    def bad_request
+      render "bad_request", formats: :json, status: :bad_request
+    end
+
+    def authorization_code_grant(
+      code = params[:code],
+      verifier = params[:code_verifier]
+    )
+      authorization = current_client.authorizations.active.find_by!(code: code)
+      return unless authorization.valid_verifier?(verifier)
+
+      authorization.issue_tokens_to(current_client)
+    end
+
+    def refresh_grant(refresh_token = params[:refresh_token])
+      jti = Token.claims_for(refresh_token, token_type: :refresh)[:jti]
+      token = Token.find(jti)
+      token.issue_tokens_to(current_client)
+    end
+
+    def password_grant(username = params[:username], password = params[:password])
+      user = User.login(username, password)
+      user.issue_tokens_to(current_client)
+    end
+
+    def assertion_grant(raw = params[:assertion])
+      assertion = Saml::Kit::Assertion.new(
+        Base64.urlsafe_decode64(raw)
+      )
+      return if assertion.invalid?
+
+      user = if assertion.name_id_format == Saml::Kit::Namespaces::PERSISTENT
+               User.find(assertion.name_id)
+             else
+               User.find_by!(email: assertion.name_id)
+             end
+      user.issue_tokens_to(current_client)
+    end
+
+    def tokens_for(grant_type = params[:grant_type])
+      case grant_type
+      when 'authorization_code'
+        authorization_code_grant
+      when 'refresh_token'
+        refresh_grant
+      when 'client_credentials'
+        [current_client.access_token, nil]
+      when 'password'
+        password_grant
+      when 'urn:ietf:params:oauth:grant-type:saml2-bearer' # RFC7522
+        assertion_grant
+        # when 'urn:ietf:params:oauth:grant-type:jwt-bearer' # RFC7523
+        # raise NotImplementedError
+      end
+    end
+
+    def revoked_tokens
+      Rails.cache.fetch("revoked-tokens", expires_in: 10.minutes) do
+        Hash[Token.revoked.pluck(:id).map { |x| [x, true] }]
+      end
+    end
+  end
+end
app/controllers/clients_controller.rb
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-class ClientsController < ApplicationController
-  skip_before_action :authenticate!
-  before_action :apply_cache_headers
-
-  def create
-    @client = Client.create!(transform(secure_params))
-    render status: :created, formats: :json
-  rescue ActiveRecord::RecordInvalid => error
-    json = {
-      error: error_type_for(error.record.errors),
-      error_description: error.record.errors.full_messages.join(' ')
-    }
-    render json: json, status: :bad_request
-  end
-
-  private
-
-  def secure_params
-    params.permit(
-      :client_name,
-      :token_endpoint_auth_method,
-      :logo_uri,
-      :jwks_uri,
-      redirect_uris: []
-    )
-  end
-
-  def transform(params)
-    {
-      name: params[:client_name],
-      redirect_uris: params[:redirect_uris],
-      token_endpoint_auth_method: params[:token_endpoint_auth_method],
-      logo_uri: params[:logo_uri],
-      jwks_uri: params[:jwks_uri],
-    }
-  end
-
-  def apply_cache_headers
-    response.headers["Cache-Control"] = "no-cache, no-store"
-    response.headers["Pragma"] = "no-cache"
-  end
-
-  def error_type_for(errors)
-    if errors[:redirect_uris].present?
-      :invalid_redirect_uri
-    else
-      :invalid_client_metadata
-    end
-  end
-end
app/controllers/oauths_controller.rb
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-class OauthsController < ApplicationController
-  VALID_RESPONSE_TYPES = %w[code token].freeze
-
-  def show
-    @client = Client.find(secure_params[:client_id])
-
-    unless @client.valid_redirect_uri?(secure_params[:redirect_uri])
-      return redirect_to @client.redirect_url(
-        error: :invalid_request,
-        state: secure_params[:state]
-      )
-    end
-
-    unless @client.valid_response_type?(secure_params[:response_type])
-      return redirect_to @client.redirect_url(
-        error: :unsupported_response_type,
-        state: secure_params[:state]
-      )
-    end
-
-    session[:oauth] = secure_params.to_h
-  end
-
-  def create(oauth = session[:oauth])
-    return render_error(:bad_request) if oauth.nil?
-
-    client = Client.find(oauth[:client_id])
-    redirect_to client.redirect_url_for(current_user, oauth)
-  rescue StandardError => error
-    logger.error(error)
-    redirect_to client.redirect_url(error: :invalid_request)
-  end
-
-  private
-
-  def secure_params
-    params.permit(
-      :client_id, :response_type, :redirect_uri,
-      :state, :code_challenge, :code_challenge_method
-    )
-  end
-end
app/controllers/tokens_controller.rb
@@ -1,109 +0,0 @@
-# frozen_string_literal: true
-
-class TokensController < ApplicationController
-  def create
-    response.headers['Cache-Control'] = 'no-store'
-    response.headers['Pragma'] = 'no-cache'
-
-    @access_token, @refresh_token = tokens_for(params[:grant_type])
-    return bad_request if @access_token.nil?
-
-    render formats: :json
-  rescue StandardError => error
-    Rails.logger.error(error)
-    bad_request
-  end
-
-  def introspect
-    claims = Token.claims_for(params[:token], token_type: :any)
-    if claims.empty? || revoked_tokens[claims[:jti]]
-      render json: { active: false }, status: :ok
-    else
-      render json: claims.merge(active: true), status: :ok
-    end
-  end
-
-  def revoke
-    claims = Token.claims_for(params[:token], token_type: :any)
-    Token.find(claims[:jti]).revoke! unless claims.empty?
-    render plain: "", status: :ok
-  rescue StandardError => error
-    logger.error(error)
-    render plain: "", status: :ok
-  end
-
-  private
-
-  attr_reader :current_client
-
-  def authenticate!
-    @current_client = authenticate_with_http_basic do |client_id, client_secret|
-      Client.find(client_id)&.authenticate(client_secret)
-    end
-    return if current_client
-
-    render "invalid_client", formats: :json, status: :unauthorized
-  end
-
-  def bad_request
-    render "bad_request", formats: :json, status: :bad_request
-  end
-
-  def authorization_code_grant(
-    code = params[:code],
-    verifier = params[:code_verifier]
-  )
-    authorization = current_client.authorizations.active.find_by!(code: code)
-    return unless authorization.valid_verifier?(verifier)
-
-    authorization.issue_tokens_to(current_client)
-  end
-
-  def refresh_grant(refresh_token = params[:refresh_token])
-    jti = Token.claims_for(refresh_token, token_type: :refresh)[:jti]
-    token = Token.find(jti)
-    token.issue_tokens_to(current_client)
-  end
-
-  def password_grant(username = params[:username], password = params[:password])
-    user = User.login(username, password)
-    user.issue_tokens_to(current_client)
-  end
-
-  def assertion_grant(raw = params[:assertion])
-    assertion = Saml::Kit::Assertion.new(
-      Base64.urlsafe_decode64(raw)
-    )
-    return if assertion.invalid?
-
-    user = if assertion.name_id_format == Saml::Kit::Namespaces::PERSISTENT
-             User.find(assertion.name_id)
-           else
-             User.find_by!(email: assertion.name_id)
-           end
-    user.issue_tokens_to(current_client)
-  end
-
-  def tokens_for(grant_type = params[:grant_type])
-    case grant_type
-    when 'authorization_code'
-      authorization_code_grant
-    when 'refresh_token'
-      refresh_grant
-    when 'client_credentials'
-      [current_client.access_token, nil]
-    when 'password'
-      password_grant
-    when 'urn:ietf:params:oauth:grant-type:saml2-bearer' # RFC7522
-      assertion_grant
-      # when 'urn:ietf:params:oauth:grant-type:jwt-bearer' # RFC7523
-      # raise NotImplementedError
-    end
-  end
-
-  def revoked_tokens
-    Rails.cache.fetch("revoked-tokens", expires_in: 10.minutes) do
-      Hash[Token.revoked.pluck(:id).map { |x| [x, true] }]
-    end
-  end
-end
app/views/oauths/show.html.erb โ†’ app/views/oauth/authorizations/show.html.erb
@@ -3,7 +3,7 @@
     <div class="col">
       <h1><%= t('.title') %></h1>
       <p><%= t('.authorize_prompt_html', name: @client.name) %></p>
-      <%= form_for :authorization, url: oauth_path, method: :post do |form| %>
+      <%= form_for :authorization, url: oauth_authorizations_path, method: :post do |form| %>
         <%= form.button t('.authorize'), type: 'submit', class: 'btn btn-primary', data: { disable_with: t('loading') } %>
       <% end %>
     </div>
app/views/clients/create.json.jbuilder โ†’ app/views/oauth/clients/create.json.jbuilder
File renamed without changes
app/views/tokens/bad_request.json.jbuilder โ†’ app/views/oauth/tokens/bad_request.json.jbuilder
File renamed without changes
app/views/tokens/create.json.jbuilder โ†’ app/views/oauth/tokens/create.json.jbuilder
File renamed without changes
app/views/tokens/invalid_client.json.jbuilder โ†’ app/views/oauth/tokens/invalid_client.json.jbuilder
File renamed without changes
config/locales/en.yml
@@ -42,11 +42,15 @@ en:
     sessions:
       index:
         title: Sessions
-  oauths:
-    show:
-      authorize: Authorize
-      authorize_prompt_html: Do you authorize <strong>%{name}</strong> to access your data?
-      title: Authorize
+  oauth:
+    authorizations:
+      show:
+        authorize: Authorize
+        authorize_prompt_html: Do you authorize <strong>%{name}</strong> to access your data?
+        title: Authorize
+    tokens:
+      bad_request:
+        invalid_request: invalid_request
   registrations:
     new:
       register: Register
@@ -54,6 +58,3 @@ en:
   sessions:
     new:
       login: Login
-  tokens:
-    bad_request:
-      invalid_request: invalid_request
config/routes.rb
@@ -5,18 +5,9 @@ Rails.application.routes.draw do
   post '/oauth/token', to: 'tokens#create'
   resource :mfa, only: [:new, :create]
   resource :metadata, only: [:show]
-  resource :oauth, only: [:show, :create] do
-    get :authorize, to: "oauths#show"
-  end
   resource :session, only: [:new, :create, :destroy]
-  resources :clients, only: [:create]
   resources :registrations, only: [:new, :create]
   resource :response, only: [:show]
-  resource :tokens, only: [:create] do
-    post :introspect
-    post :revoke
-  end
-
   namespace :my do
     resource :dashboard, only: [:show]
     resource :mfa, only: [:show, :new, :edit, :create, :destroy]
@@ -24,7 +15,14 @@ Rails.application.routes.draw do
     resources :clients, only: [:index, :new, :create]
     resources :sessions, only: [:index]
   end
-
+  namespace :oauth do
+    resource :authorizations, only: [:show, :create]
+    resources :clients, only: [:create]
+    resource :tokens, only: [:create] do
+      post :introspect
+      post :revoke
+    end
+  end
   namespace :scim do
     namespace :v2, defaults: { format: :scim } do
       post ".search", to: "search#index"
spec/requests/oauth_spec.rb โ†’ spec/requests/oauth/authorizations_spec.rb
@@ -2,68 +2,55 @@
 
 require 'rails_helper'
 
-RSpec.describe '/oauth' do
+RSpec.describe '/oauth/authorizations' do
   context "when the user is logged in" do
     let(:current_user) { create(:user) }
 
     before { http_login(current_user) }
 
-    describe "GET /oauth" do
+    describe "GET /oauth/authorizations" do
       let(:state) { SecureRandom.uuid }
 
       context "when the client id is known" do
         let(:client) { create(:client) }
 
         context "when requesting an authorization code" do
-          before { get "/oauth", params: { client_id: client.to_param, response_type: 'code', state: state, redirect_uri: client.redirect_uris[0] } }
+          before { get "/oauth/authorizations", params: { client_id: client.to_param, response_type: 'code', state: state, redirect_uri: client.redirect_uris[0] } }
 
           specify { expect(response).to have_http_status(:ok) }
           specify { expect(response.body).to include(CGI.escapeHTML(client.name)) }
         end
 
         context "when requesting an access token" do
-          before { get "/oauth", params: { client_id: client.to_param, response_type: 'token', state: state, redirect_uri: client.redirect_uris[0] } }
+          before { get "/oauth/authorizations", params: { client_id: client.to_param, response_type: 'token', state: state, redirect_uri: client.redirect_uris[0] } }
 
           specify { expect(response).to have_http_status(:ok) }
           specify { expect(response.body).to include(CGI.escapeHTML(client.name)) }
         end
 
         context "when an incorrect response_type is provided" do
-          before { get "/oauth", params: { client_id: client.to_param, response_type: 'invalid', redirect_uri: client.redirect_uris[0] } }
+          before { get "/oauth/authorizations", params: { client_id: client.to_param, response_type: 'invalid', redirect_uri: client.redirect_uris[0] } }
 
           specify { expect(response).to redirect_to("#{client.redirect_uris[0]}#error=unsupported_response_type") }
         end
 
         context "when the redirect uri does not match" do
-          before { get "/oauth", params: { client_id: client.to_param, response_type: 'invalid', redirect_uri: SecureRandom.uuid } }
+          before { get "/oauth/authorizations", params: { client_id: client.to_param, response_type: 'invalid', redirect_uri: SecureRandom.uuid } }
 
           specify { expect(response).to redirect_to("#{client.redirect_uris[0]}#error=invalid_request") }
         end
       end
     end
 
-    describe "GET /oauth/authorize" do
-      let(:state) { SecureRandom.uuid }
-
-      context "when the client id is known" do
-        let(:client) { create(:client) }
-
-        before { get "/oauth/authorize", params: { client_id: client.to_param, response_type: 'code', state: state, redirect_uri: client.redirect_uris[0] } }
-
-        specify { expect(response).to have_http_status(:ok) }
-        specify { expect(response.body).to include(CGI.escapeHTML(client.name)) }
-      end
-    end
-
-    describe "POST /oauth" do
+    describe "POST /oauth/authorizations" do
       context "when the client id is known" do
         let(:client) { create(:client) }
         let(:state) { SecureRandom.uuid }
 
         context "when the client requested an authorization code" do
           before do
-            get "/oauth", params: { client_id: client.to_param, response_type: 'code', state: state, redirect_uri: client.redirect_uris[0] }
-            post "/oauth"
+            get "/oauth/authorizations", params: { client_id: client.to_param, response_type: 'code', state: state, redirect_uri: client.redirect_uris[0] }
+            post "/oauth/authorizations"
           end
 
           specify { expect(response).to redirect_to(client.redirect_url(code: Authorization.last.code, state: state)) }
@@ -74,8 +61,8 @@ RSpec.describe '/oauth' do
           let(:scope) { "admin" }
 
           before do
-            get "/oauth", params: { client_id: client.to_param, response_type: 'token', state: state, redirect_uri: client.redirect_uris[0] }
-            post "/oauth"
+            get "/oauth/authorizations", params: { client_id: client.to_param, response_type: 'token', state: state, redirect_uri: client.redirect_uris[0] }
+            post "/oauth/authorizations"
           end
 
           specify { expect(response).to redirect_to("#{client.redirect_uris[0]}#access_token=#{token}&token_type=Bearer&expires_in=300&scope=#{scope}&state=#{state}") }
@@ -87,7 +74,7 @@ RSpec.describe '/oauth' do
           let(:code_challenge) { Base64.urlsafe_encode64(Digest::SHA256.hexdigest(code_verifier)) }
 
           before do
-            get "/oauth", params: {
+            get "/oauth/authorizations", params: {
               client_id: client.to_param,
               response_type: 'code',
               code_challenge: code_challenge,
@@ -95,7 +82,7 @@ RSpec.describe '/oauth' do
               state: state,
               redirect_uri: client.redirect_uris[0]
             }
-            post "/oauth"
+            post "/oauth/authorizations"
           end
 
           specify { expect(response).to redirect_to(client.redirect_url(code: Authorization.last.code, state: state)) }
@@ -108,7 +95,7 @@ RSpec.describe '/oauth' do
           let(:code_verifier) { SecureRandom.hex(128) }
 
           before do
-            get "/oauth", params: {
+            get "/oauth/authorizations", params: {
               client_id: client.to_param,
               response_type: 'code',
               code_challenge: code_verifier,
@@ -116,7 +103,7 @@ RSpec.describe '/oauth' do
               state: state,
               redirect_uri: client.redirect_uris[0]
             }
-            post "/oauth"
+            post "/oauth/authorizations"
           end
 
           specify { expect(response).to redirect_to(client.redirect_url(code: Authorization.last.code, state: state)) }
@@ -129,14 +116,14 @@ RSpec.describe '/oauth' do
           let(:code_verifier) { SecureRandom.hex(128) }
 
           before do
-            get "/oauth", params: {
+            get "/oauth/authorizations", params: {
               client_id: client.to_param,
               response_type: 'code',
               code_challenge: code_verifier,
               state: state,
               redirect_uri: client.redirect_uris[0]
             }
-            post "/oauth"
+            post "/oauth/authorizations"
           end
 
           specify { expect(response).to redirect_to(client.redirect_url(code: Authorization.last.code, state: state)) }
@@ -145,7 +132,7 @@ RSpec.describe '/oauth' do
         end
 
         context "when the client did not make an appropriate request" do
-          before { post "/oauth" }
+          before { post "/oauth/authorizations" }
 
           specify { expect(response).to have_http_status(:bad_request) }
         end
@@ -154,8 +141,8 @@ RSpec.describe '/oauth' do
           let(:state) { "<script>alert('hi');</script>" }
 
           before do
-            get "/oauth", params: { client_id: client.to_param, response_type: 'token', state: state, redirect_uri: client.redirect_uris[0] }
-            post "/oauth"
+            get "/oauth/authorizations", params: { client_id: client.to_param, response_type: 'token', state: state, redirect_uri: client.redirect_uris[0] }
+            post "/oauth/authorizations"
           end
 
           specify { expect(response).to redirect_to(client.redirect_url(error: 'invalid_request')) }
spec/requests/clients_spec.rb โ†’ spec/requests/oauth/clients_spec.rb
@@ -2,8 +2,8 @@
 
 require 'rails_helper'
 
-RSpec.describe "/clients" do
-  describe "POST /clients" do
+RSpec.describe "/oauth/clients" do
+  describe "POST /oauth/clients" do
     let(:redirect_uris) { [generate(:uri), generate(:uri)] }
     let(:client_name) { FFaker::Name.name }
     let(:logo_uri) { generate(:uri) }
@@ -13,7 +13,7 @@ RSpec.describe "/clients" do
 
     context "when the registration request is valid" do
       before do
-        post "/clients", params: {
+        post "/oauth/clients", params: {
           redirect_uris: redirect_uris,
           client_name: client_name,
           token_endpoint_auth_method: :client_secret_basic,
@@ -40,7 +40,7 @@ RSpec.describe "/clients" do
 
     context "when the registrations is missing valid redirect_uris" do
       before do
-        post "/clients", params: {
+        post "/oauth/clients", params: {
           redirect_uris: [],
           client_name: client_name,
           token_endpoint_auth_method: :client_secret_basic,
@@ -56,7 +56,7 @@ RSpec.describe "/clients" do
 
     context "when the registration request is missing a client name" do
       before do
-        post "/clients", params: {
+        post "/oauth/clients", params: {
           redirect_uris: redirect_uris,
           client_name: "",
           token_endpoint_auth_method: :client_secret_basic,
spec/requests/tokens_spec.rb โ†’ spec/requests/oauth/tokens_spec.rb
@@ -2,18 +2,18 @@
 
 require 'rails_helper'
 
-RSpec.describe '/tokens' do
+RSpec.describe '/oauth/tokens' do
   let(:client) { create(:client) }
   let(:credentials) { ActionController::HttpAuthentication::Basic.encode_credentials(client.to_param, client.password) }
   let(:headers) { { 'Authorization' => credentials } }
 
-  describe "POST /oauth/token" do
+  describe "POST /oauth/tokens" do
     context "when using the authorization_code grant" do
       context "when the code is still valid" do
         let(:authorization) { create(:authorization, client: client) }
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
 
         specify { expect(response).to have_http_status(:ok) }
         specify { expect(response.headers['Content-Type']).to include('application/json') }
@@ -30,7 +30,7 @@ RSpec.describe '/tokens' do
         let(:authorization) { create(:authorization, client: client, expired_at: 1.second.ago) }
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
 
         specify { expect(response).to have_http_status(:bad_request) }
         specify { expect(response.headers['Content-Type']).to include('application/json') }
@@ -40,7 +40,7 @@ RSpec.describe '/tokens' do
       end
 
       context "when the code is not known" do
-        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
 
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
@@ -58,8 +58,8 @@ RSpec.describe '/tokens' do
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
         before do
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, headers: headers
         end
 
         specify { expect(response).to have_http_status(:ok) }
@@ -79,8 +79,8 @@ RSpec.describe '/tokens' do
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
         before do
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, headers: headers
         end
 
         specify { expect(response).to have_http_status(:ok) }
@@ -100,8 +100,8 @@ RSpec.describe '/tokens' do
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
         before do
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers
         end
 
         specify { expect(response).to have_http_status(:bad_request) }
@@ -118,8 +118,8 @@ RSpec.describe '/tokens' do
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
         before do
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
-          post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
+          post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers
         end
 
         specify { expect(response).to have_http_status(:bad_request) }
@@ -135,7 +135,7 @@ RSpec.describe '/tokens' do
       context "when the client credentials are valid" do
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-        before { post '/oauth/token', params: { grant_type: 'client_credentials' }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'client_credentials' }, headers: headers }
 
         specify { expect(response).to have_http_status(:ok) }
         specify { expect(response.headers['Content-Type']).to include('application/json') }
@@ -151,7 +151,7 @@ RSpec.describe '/tokens' do
         let(:headers) { { 'Authorization' => 'invalid' } }
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-        before { post '/oauth/token', params: { grant_type: 'client_credentials' }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'client_credentials' }, headers: headers }
 
         specify { expect(response).to have_http_status(:unauthorized) }
         specify { expect(json[:error]).to eql('invalid_client') }
@@ -163,7 +163,7 @@ RSpec.describe '/tokens' do
         let(:user) { create(:user) }
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-        before { post '/oauth/token', params: { grant_type: 'password', username: user.email, password: user.password }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'password', username: user.email, password: user.password }, headers: headers }
 
         specify { expect(response).to have_http_status(:ok) }
         specify { expect(response.headers['Content-Type']).to include('application/json') }
@@ -178,7 +178,7 @@ RSpec.describe '/tokens' do
       context "when the credentials are invalid" do
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-        before { post '/oauth/token', params: { grant_type: 'password', username: generate(:email), password: generate(:password) }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'password', username: generate(:email), password: generate(:password) }, headers: headers }
 
         specify { expect(response).to have_http_status(:bad_request) }
         specify { expect(json[:error]).to eql('invalid_request') }
@@ -190,7 +190,7 @@ RSpec.describe '/tokens' do
         let(:refresh_token) { create(:refresh_token) }
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-        before { post '/oauth/token', params: { grant_type: 'refresh_token', refresh_token: refresh_token.to_jwt }, headers: headers }
+        before { post '/oauth/tokens', params: { grant_type: 'refresh_token', refresh_token: refresh_token.to_jwt }, headers: headers }
 
         specify { expect(response).to have_http_status(:ok) }
         specify { expect(response.headers['Content-Type']).to include('application/json') }
@@ -214,7 +214,7 @@ RSpec.describe '/tokens' do
 
         before do
           allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-          post '/oauth/token', params: {
+          post '/oauth/tokens', params: {
             grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
             assertion: Base64.urlsafe_encode64(saml),
           }, headers: headers
@@ -239,7 +239,7 @@ RSpec.describe '/tokens' do
 
         before do
           allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-          post '/oauth/token', params: {
+          post '/oauth/tokens', params: {
             grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
             assertion: Base64.urlsafe_encode64(saml),
           }, headers: headers
@@ -265,7 +265,7 @@ RSpec.describe '/tokens' do
 
       before do
         allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-        post '/oauth/token', params: {
+        post '/oauth/tokens', params: {
           grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
           assertion: Base64.urlsafe_encode64(saml),
         }, headers: headers
@@ -288,7 +288,7 @@ RSpec.describe '/tokens' do
 
       before do
         allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-        post '/oauth/token', params: {
+        post '/oauth/tokens', params: {
           grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
           assertion: Base64.urlsafe_encode64(saml),
         }, headers: headers
@@ -303,12 +303,12 @@ RSpec.describe '/tokens' do
     end
   end
 
-  describe "POST /tokens/introspect" do
+  describe "POST /oauth/tokens/introspect" do
     context "when the access_token is valid" do
       let(:token) { create(:access_token) }
       let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-      before { post '/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
+      before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
 
       specify { expect(response).to have_http_status(:ok) }
       specify { expect(response['Content-Type']).to include('application/json') }
@@ -324,7 +324,7 @@ RSpec.describe '/tokens' do
       let(:token) { create(:refresh_token) }
       let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-      before { post '/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
+      before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
 
       specify { expect(response).to have_http_status(:ok) }
       specify { expect(response['Content-Type']).to include('application/json') }
@@ -340,7 +340,7 @@ RSpec.describe '/tokens' do
       let(:token) { create(:access_token, :revoked) }
       let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-      before { post '/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
+      before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
 
       specify { expect(response).to have_http_status(:ok) }
       specify { expect(response['Content-Type']).to include('application/json') }
@@ -351,7 +351,7 @@ RSpec.describe '/tokens' do
       let(:token) { create(:access_token, :expired) }
       let(:json) { JSON.parse(response.body, symbolize_names: true) }
 
-      before { post '/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
+      before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
 
       specify { expect(response).to have_http_status(:ok) }
       specify { expect(response['Content-Type']).to include('application/json') }
@@ -359,12 +359,12 @@ RSpec.describe '/tokens' do
     end
   end
 
-  describe "POST /tokens/revoke" do
+  describe "POST /oauth/tokens/revoke" do
     context "when the client credentials are valid" do
       context "when the access token is active and known" do
         let(:token) { create(:access_token) }
 
-        before { post '/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :access_token }, headers: headers }
+        before { post '/oauth/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :access_token }, headers: headers }
 
         specify { expect(response).to have_http_status(:ok) }
         specify { expect(response.body).to be_empty }
@@ -374,7 +374,7 @@ RSpec.describe '/tokens' do
       context "when the refresh token is active and known" do
         let(:token) { create(:refresh_token) }
 
-        before { post '/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :refresh_token }, headers: headers }
+        before { post '/oauth/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :refresh_token }, headers: headers }
 
         specify { expect(response).to have_http_status(:ok) }
         specify { expect(response.body).to be_empty }
@@ -384,7 +384,7 @@ RSpec.describe '/tokens' do
       context "when the access token is expired" do
         let(:token) { create(:access_token, :expired) }
 
-        before { post '/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :refresh_token }, headers: headers }
+        before { post '/oauth/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :refresh_token }, headers: headers }
 
         specify { expect(response).to have_http_status(:ok) }
       end