Add support for scopes

Remove scopes from tokens

Squashed commits:

[83db38f] Add redirect uris to supported metadata
This commit is contained in:
theworldbright 2015-07-27 18:26:41 +09:00
parent de4f68c289
commit 3cbe75469b
22 changed files with 165 additions and 171 deletions

View file

@ -1,6 +1,7 @@
class Api::V0::BaseController < ApplicationController
include OpenidConnect::ProtectedResourceEndpoint
before_filter :require_access_token
def user
current_token ? current_token.authorization.user : nil
end
end

View file

@ -1,11 +1,9 @@
class Api::V0::UsersController < Api::V0::BaseController
before_filter do
require_access_token OpenidConnect::Scope.find_by(name: "read")
end
def show
render json: user
end
private
def user
current_token.authorization.user
end
end

View file

@ -47,7 +47,7 @@ class OpenidConnect::AuthorizationsController < ApplicationController
session[:client_id] = @o_auth_application.client_id
session[:response_type] = @response_type
session[:redirect_uri] = @redirect_uri
session[:scopes] = @scopes.map(&:name)
session[:scopes] = @scopes.map(&:name).join(" ")
session[:request_object] = @request_object
session[:nonce] = params[:nonce]
end
@ -84,7 +84,7 @@ class OpenidConnect::AuthorizationsController < ApplicationController
req.update_param("response_type", session[:response_type].respond_to?(:map) ?
session[:response_type].map(&:to_s).join(" ") :
session[:response_type])
req.update_param("scopes", session[:scopes])
req.update_param("scope", session[:scopes])
req.update_param("request_object", session[:request_object])
req.update_param("nonce", session[:nonce])
end

View file

@ -6,6 +6,7 @@ class OpenidConnect::Authorization < ActiveRecord::Base
validates :o_auth_application, presence: true
validates :user, uniqueness: {scope: :o_auth_application}
has_many :authorization_scopes
has_many :scopes, through: :authorization_scopes
has_many :o_auth_access_tokens, dependent: :destroy
has_many :id_tokens, dependent: :destroy
@ -16,6 +17,12 @@ class OpenidConnect::Authorization < ActiveRecord::Base
self.refresh_token = SecureRandom.hex(32)
end
def accessible?(required_scopes=nil)
Array(required_scopes).all? do |required_scope|
scopes.include? required_scope
end
end
def create_access_token
o_auth_access_tokens.create!.bearer_token
# TODO: Add support for request object

View file

@ -1,6 +1,5 @@
class OpenidConnect::OAuthAccessToken < ActiveRecord::Base
belongs_to :authorization
has_many :scopes, through: :scope_tokens
before_validation :setup, on: :create
@ -20,8 +19,4 @@ class OpenidConnect::OAuthAccessToken < ActiveRecord::Base
expires_in: (expires_at - Time.now.utc).to_i
)
end
def accessible?(_scopes_or_claims_=nil)
true # TODO: For now don't support scopes
end
end

View file

@ -44,7 +44,7 @@ class OpenidConnect::OAuthApplication < ActiveRecord::Base
def supported_metadata
%i(client_name response_types grant_types application_type
contacts logo_uri client_uri policy_uri tos_uri)
contacts logo_uri client_uri policy_uri tos_uri redirect_uris)
end
def registrar_attributes(registrar)

View file

@ -1,8 +1,7 @@
class OpenidConnect::Scope < ActiveRecord::Base
has_many :o_auth_access_token, through: :scope_tokens
has_many :authorizations, through: :authorization_scopes
validates :name, presence: true, uniqueness: true
# TODO: Incomplete class
# TODO: Add constants so scopes can be referenced as OpenidConnect::Scope::Read
end

View file

@ -1,7 +0,0 @@
class OpenidConnect::ScopeToken < ActiveRecord::Base
belongs_to :scope
belongs_to :o_auth_access_token
validates :scope, presence: true
validates :token, presence: true
end

View file

@ -0,0 +1,8 @@
class CreateAuthorizationScopesJoinTable < ActiveRecord::Migration
def change
create_table :authorization_scopes, id: false do |t|
t.belongs_to :authorization, index: true
t.belongs_to :scope, index: true
end
end
end

View file

@ -1,8 +0,0 @@
class CreateAuthorizationsScopesJoinTable < ActiveRecord::Migration
def change
create_table :authorizations_scopes, id: false do |t|
t.belongs_to :authorization, index: true
t.belongs_to :scope, index: true
end
end
end

View file

@ -1,8 +0,0 @@
class CreateScopesTokensJoinTable < ActiveRecord::Migration
def change
create_table :scopes_tokens, id: false do |t|
t.belongs_to :scope, index: true
t.belongs_to :o_auth_access_token, index: true
end
end
end

View file

@ -55,6 +55,14 @@ ActiveRecord::Schema.define(version: 20150724152052) do
add_index "aspects", ["user_id", "contacts_visible"], name: "index_aspects_on_user_id_and_contacts_visible", using: :btree
add_index "aspects", ["user_id"], name: "index_aspects_on_user_id", using: :btree
create_table "authorization_scopes", id: false, force: :cascade do |t|
t.integer "authorization_id", limit: 4
t.integer "scope_id", limit: 4
end
add_index "authorization_scopes", ["authorization_id"], name: "index_authorization_scopes_on_authorization_id", using: :btree
add_index "authorization_scopes", ["scope_id"], name: "index_authorization_scopes_on_scope_id", using: :btree
create_table "authorizations", force: :cascade do |t|
t.integer "user_id", limit: 4
t.integer "o_auth_application_id", limit: 4
@ -66,14 +74,6 @@ ActiveRecord::Schema.define(version: 20150724152052) do
add_index "authorizations", ["o_auth_application_id"], name: "index_authorizations_on_o_auth_application_id", using: :btree
add_index "authorizations", ["user_id"], name: "index_authorizations_on_user_id", using: :btree
create_table "authorizations_scopes", id: false, force: :cascade do |t|
t.integer "authorization_id", limit: 4
t.integer "scope_id", limit: 4
end
add_index "authorizations_scopes", ["authorization_id"], name: "index_authorizations_scopes_on_authorization_id", using: :btree
add_index "authorizations_scopes", ["scope_id"], name: "index_authorizations_scopes_on_scope_id", using: :btree
create_table "blocks", force: :cascade do |t|
t.integer "user_id", limit: 4
t.integer "person_id", limit: 4
@ -523,14 +523,6 @@ ActiveRecord::Schema.define(version: 20150724152052) do
t.datetime "updated_at", null: false
end
create_table "scopes_tokens", id: false, force: :cascade do |t|
t.integer "scope_id", limit: 4
t.integer "o_auth_access_token_id", limit: 4
end
add_index "scopes_tokens", ["o_auth_access_token_id"], name: "index_scopes_tokens_on_o_auth_access_token_id", using: :btree
add_index "scopes_tokens", ["scope_id"], name: "index_scopes_tokens_on_scope_id", using: :btree
create_table "services", force: :cascade do |t|
t.string "type", limit: 127, null: false
t.integer "user_id", limit: 4, null: false

View file

@ -1,6 +1,7 @@
Feature: Access protected resources using password flow
Background:
Given a user with username "kent"
And all scopes exist
Scenario: Invalid credentials to token endpoint
When I register a new client

View file

@ -2,7 +2,7 @@
Feature: Access protected resources using implicit flow
Background:
Given a user with username "kent"
And the OpenID scope exists
And all scopes exist
Scenario: Invalid client id to auth endpoint
When I register a new client

View file

@ -1,24 +1,18 @@
o_auth_query_params = %i(
redirect_uri=http://localhost:3000
response_type=id_token token
scope=openid
response_type=id_token%20token
scope=openid%20read
nonce=hello
state=hi
).join("&")
Given(/^the OpenID scope exists$/) do
OpenidConnect::Scope.create(name: "openid")
end
Given /^I send a post request from that client to the implicit flow authorization endpoint$/ do
client_json = JSON.parse(last_response.body)
auth_endpoint_url = "/openid_connect/authorizations/new"
visit "#{auth_endpoint_url}?client_id=#{client_json["o_auth_application"]["client_id"]}&#{o_auth_query_params}"
visit new_openid_connect_authorization_path + "?client_id=#{client_json["o_auth_application"]["client_id"]}&#{o_auth_query_params}"
end
Given /^I send a post request from that client to the implicit flow authorization endpoint using a invalid client id/ do
auth_endpoint_url = "/openid_connect/authorizations/new"
visit "#{auth_endpoint_url}?client_id=randomid&#{o_auth_query_params}"
visit new_openid_connect_authorization_path + "?client_id=randomid&#{o_auth_query_params}"
end
When /^I give my consent and authorize the client$/ do
@ -38,8 +32,7 @@ end
When /^I parse the bearer tokens and use it to access user info$/ do
access_token = current_url[/(?<=access_token=)[^&]+/]
user_info_endpoint_url = "/api/v0/user/"
get user_info_endpoint_url, access_token: access_token
get api_v0_user_path, access_token: access_token
end
Then /^I should see an "([^\"]*)" error$/ do |error_message|

View file

@ -1,35 +1,37 @@
Given(/^all scopes exist$/) do
OpenidConnect::Scope.find_or_create_by(name: "openid")
OpenidConnect::Scope.find_or_create_by(name: "read")
end
When /^I register a new client$/ do
client_registration_url = "/openid_connect/clients"
post client_registration_url, redirect_uris: ["http://localhost:3000"], client_name: "diaspora client"
post openid_connect_clients_path, redirect_uris: ["http://localhost:3000"], client_name: "diaspora client"
end
Given /^I send a post request from that client to the password flow token endpoint using "([^\"]*)"'s credentials$/ do |username|
client_json = JSON.parse(last_response.body)
user = User.find_by(username: username)
token_endpoint_url = "/openid_connect/access_tokens"
post token_endpoint_url, grant_type: "password", username: user.username,
post openid_connect_access_tokens_path, grant_type: "password", username: user.username,
password: "password", # Password has been hard coded as all test accounts seem to have a password of "password"
client_id: client_json["o_auth_application"]["client_id"],
client_secret: client_json["o_auth_application"]["client_secret"]
client_secret: client_json["o_auth_application"]["client_secret"],
scope: "read"
end
Given /^I send a post request from that client to the password flow token endpoint using invalid credentials$/ do
client_json = JSON.parse(last_response.body)
token_endpoint_url = "/openid_connect/access_tokens"
post token_endpoint_url, grant_type: "password", username: "bob", password: "wrongpassword",
post openid_connect_access_tokens_path, grant_type: "password", username: "bob", password: "wrongpassword",
client_id: client_json["o_auth_application"]["client_id"],
client_secret: client_json["o_auth_application"]["client_secret"]
client_secret: client_json["o_auth_application"]["client_secret"],
scope: "read"
end
When /^I use received valid bearer tokens to access user info$/ do
access_token_json = JSON.parse(last_response.body)
user_info_endpoint_url = "/api/v0/user/"
get user_info_endpoint_url, access_token: access_token_json["access_token"]
get api_v0_user_path, access_token: access_token_json["access_token"]
end
When /^I use invalid bearer tokens to access user info$/ do
user_info_endpoint_url = "/api/v0/user/"
get user_info_endpoint_url, access_token: SecureRandom.hex(32)
get api_v0_user_path, access_token: SecureRandom.hex(32)
end
Then /^I should receive "([^\"]*)"'s id, username, and email$/ do |username|

View file

@ -45,8 +45,8 @@ module OpenidConnect
end
def build_scopes(req)
@scopes = req.scope.map {|scope|
OpenidConnect::Scope.where(name: scope).first.tap do |scope|
@scopes = req.scope.map {|scope_name|
OpenidConnect::Scope.where(name: scope_name).first.tap do |scope|
req.invalid_scope! "Unknown scope: #{scope}" unless scope
end
}

View file

@ -21,6 +21,7 @@ module OpenidConnect
# TODO: Add support for request object and auth code
def approved!(req, res)
auth = OpenidConnect::Authorization.find_or_create_by(o_auth_application: @o_auth_application, user: @user)
auth.scopes << @scopes
response_types = Array(req.response_type)
if response_types.include?(:token)
res.access_token = auth.create_access_token

View file

@ -2,20 +2,14 @@ module OpenidConnect
module ProtectedResourceEndpoint
attr_reader :current_token
def require_access_token
def require_access_token(*required_scopes)
@current_token = request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
unless @current_token
unless @current_token && @current_token.authorization
raise Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new("Unauthorized user")
end
# TODO: This block is useless until we actually start checking for scopes
unless @current_token.try(:accessible?, required_scopes)
unless @current_token.authorization.try(:accessible?, required_scopes)
raise Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(:insufficient_scope)
end
end
# TODO: Scopes should be implemented here
def required_scopes
nil
end
end
end

View file

@ -29,7 +29,13 @@ module OpenidConnect
user = User.find_for_database_authentication(username: req.username)
if user
if user.valid_password?(req.password)
scope_list = req.scope.map { |scope_name|
OpenidConnect::Scope.find_by(name: scope_name).tap do |scope|
req.invalid_scope! "Unknown scope: #{scope}" unless scope
end
} # TODO: Check client scope permissions
auth = OpenidConnect::Authorization.find_or_create_by(o_auth_application: o_auth_app, user: user)
auth.scopes << scope_list
res.access_token = auth.create_access_token
else
req.invalid_grant!
@ -40,6 +46,8 @@ module OpenidConnect
end
def handle_refresh_flow(req, res)
# Handle as if scope request was omitted even if provided.
# See https://tools.ietf.org/html/rfc6749#section-6 for handling
auth = OpenidConnect::Authorization.find_by_refresh_token req.client_id, req.refresh_token
if auth
res.access_token = auth.create_access_token

View file

@ -1,75 +1,89 @@
require "spec_helper"
describe OpenidConnect::ProtectedResourceEndpoint, type: :request do
describe "getting the user info" do
let!(:client) do
OpenidConnect::OAuthApplication.create!(
client_name: "Diaspora Test Client", redirect_uris: ["http://localhost:3000/"])
end
let!(:auth) { OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: bob) }
let!(:access_token) { auth.create_access_token.to_s }
let!(:invalid_token) { SecureRandom.hex(32).to_s }
# TODO: Add tests for expired access tokens
let!(:client) do
OpenidConnect::OAuthApplication.create!(
client_name: "Diaspora Test Client", redirect_uris: ["http://localhost:3000/"])
end
let(:auth_with_read) do
auth = OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: bob)
auth.scopes << [OpenidConnect::Scope.find_or_create_by(name: "read")]
auth
end
let!(:access_token_with_read) { auth_with_read.create_access_token.to_s }
let(:auth_with_read_and_write) do
auth = OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: bob)
auth.scopes << [OpenidConnect::Scope.find_or_create_by(name: "read"), OpenidConnect::Scope.find_or_create_by(name: "write")]
auth
end
let!(:access_token_with_read_and_write) { auth_with_read_and_write.create_access_token.to_s }
let(:invalid_token) { SecureRandom.hex(32).to_s }
context "when access token is valid" do
it "shows the user's username and email" do
get "/api/v0/user/", access_token: access_token
# TODO: Add tests for expired access tokens
context "when read scope access token is provided for read required endpoint" do
describe "user info endpoint" do
before do
get api_v0_user_path, access_token: access_token_with_read
end
it "shows the info" do
json_body = JSON.parse(response.body)
expect(json_body["username"]).to eq(bob.username)
expect(json_body["email"]).to eq(bob.email)
end
it "should include private in the cache-control header" do
get "/api/v0/user/", access_token: access_token
it "includes private in the cache-control header" do
expect(response.headers["Cache-Control"]).to include("private")
end
end
end
context "when no access token is provided" do
it "should respond with a 401 Unauthorized response" do
get "/api/v0/user/"
expect(response.status).to be(401)
end
it "should have an auth-scheme value of Bearer" do
get "/api/v0/user/"
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
end
context "when no access token is provided" do
it "should respond with a 401 Unauthorized response" do
get api_v0_user_path
expect(response.status).to be(401)
end
it "should have an auth-scheme value of Bearer" do
get api_v0_user_path
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
end
end
context "when an invalid access token is provided" do
before do
get api_v0_user_path, access_token: invalid_token
end
context "when an invalid access token is provided" do
before do
get "/api/v0/user/", access_token: invalid_token
end
it "should respond with a 401 Unauthorized response" do
expect(response.status).to be(401)
end
it "should have an auth-scheme value of Bearer" do
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
end
it "should contain an invalid_token error" do
expect(response.body).to include("invalid_token")
end
it "should respond with a 401 Unauthorized response" do
expect(response.status).to be(401)
end
context "when authorization has been destroyed" do
before do
auth.destroy
get "/api/v0/user/", access_token: access_token
end
it "should have an auth-scheme value of Bearer" do
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
end
it "should respond with a 401 Unauthorized response" do
expect(response.status).to be(401)
end
it "should contain an invalid_token error" do
expect(response.body).to include("invalid_token")
end
end
it "should have an auth-scheme value of Bearer" do
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
end
context "when authorization has been destroyed" do
before do
auth_with_read.destroy
get api_v0_user_path, access_token: access_token_with_read
end
it "should contain an invalid_token error" do
expect(response.body).to include("invalid_token")
end
it "should respond with a 401 Unauthorized response" do
expect(response.status).to be(401)
end
it "should have an auth-scheme value of Bearer" do
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
end
it "should contain an invalid_token error" do
expect(response.body).to include("invalid_token")
end
end
end

View file

@ -5,53 +5,57 @@ describe OpenidConnect::TokenEndpoint, type: :request do
OpenidConnect::OAuthApplication.create!(
redirect_uris: ["http://localhost"], client_name: "diaspora client")
end
let!(:auth) { OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: bob) }
let(:auth) { OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: bob) }
before do
OpenidConnect::Scope.find_or_create_by(name: "read")
end
describe "the password grant type" do
context "when the username field is missing" do
it "should return an invalid request error" do
post "/openid_connect/access_tokens", grant_type: "password", password: "bluepin7",
client_id: client.client_id, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "password", password: "bluepin7",
client_id: client.client_id, client_secret: client.client_secret, scope: "read"
expect(response.body).to include "'username' required"
end
end
context "when the password field is missing" do
it "should return an invalid request error" do
post "/openid_connect/access_tokens", grant_type: "password", username: "bob",
client_id: client.client_id, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "password", username: "bob",
client_id: client.client_id, client_secret: client.client_secret, scope: "read"
expect(response.body).to include "'password' required"
end
end
context "when the username does not match an existing user" do
it "should return an invalid request error" do
post "/openid_connect/access_tokens", grant_type: "password", username: "randomnoexist",
password: "bluepin7", client_id: client.client_id, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "password", username: "randomnoexist",
password: "bluepin7", client_id: client.client_id, client_secret: client.client_secret, scope: "read"
expect(response.body).to include "invalid_grant"
end
end
context "when the password is invalid" do
it "should return an invalid request error" do
post "/openid_connect/access_tokens", grant_type: "password", username: "bob",
password: "wrongpassword", client_id: client.client_id, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "password", username: "bob",
password: "wrongpassword", client_id: client.client_id, client_secret: client.client_secret, scope: "read"
expect(response.body).to include "invalid_grant"
end
end
context "when the client_secret doesn't match" do
it "should return an invalid client error" do
post "/openid_connect/access_tokens", grant_type: "password", username: "bob",
password: "bluepin7", client_id: client.client_id, client_secret: "client.client_secret"
post openid_connect_access_tokens_path, grant_type: "password", username: "bob",
password: "bluepin7", client_id: client.client_id, client_secret: "client.client_secret", scope: "read"
expect(response.body).to include "invalid_client"
end
end
context "when the request is valid" do
it "should return an access token" do
post "/openid_connect/access_tokens", grant_type: "password", username: "bob",
password: "bluepin7", client_id: client.client_id, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "password", username: "bob",
password: "bluepin7", client_id: client.client_id, client_secret: client.client_secret, scope: "read"
json = JSON.parse(response.body)
expect(json.keys).to include "expires_in"
expect(json["access_token"].length).to eq(64)
@ -61,16 +65,16 @@ describe OpenidConnect::TokenEndpoint, type: :request do
context "when there are duplicate fields" do
it "should return an invalid request error" do
post "/openid_connect/access_tokens", grant_type: "password", username: "bob", password: "bluepin7",
username: "bob", password: "bluepin6", client_id: client.client_id, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "password", username: "bob", password: "bluepin7",
username: "bob", password: "bluepin6", client_id: client.client_id, client_secret: client.client_secret, scope: "read"
expect(response.body).to include "invalid_grant"
end
end
context "when the client is unregistered" do
it "should return an error" do
post "/openid_connect/access_tokens", grant_type: "password", username: "bob",
password: "bluepin7", client_id: SecureRandom.hex(16).to_s, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "password", username: "bob",
password: "bluepin7", client_id: SecureRandom.hex(16).to_s, client_secret: client.client_secret, scope: "read"
expect(response.body).to include "invalid_client"
end
end
@ -80,8 +84,8 @@ describe OpenidConnect::TokenEndpoint, type: :request do
describe "an unsupported grant type" do
it "should return an unsupported grant type error" do
post "/openid_connect/access_tokens", grant_type: "noexistgrant", username: "bob",
password: "bluepin7", client_id: client.client_id, client_secret: client.client_secret
post openid_connect_access_tokens_path, grant_type: "noexistgrant", username: "bob",
password: "bluepin7", client_id: client.client_id, client_secret: client.client_secret, scope: "read"
expect(response.body).to include "unsupported_grant_type"
end
end
@ -89,7 +93,7 @@ describe OpenidConnect::TokenEndpoint, type: :request do
describe "the refresh token flow" do
context "when the refresh token is valid" do
it "should return an access token" do
post "/openid_connect/access_tokens", grant_type: "refresh_token",
post openid_connect_access_tokens_path, grant_type: "refresh_token",
client_id: client.client_id, client_secret: client.client_secret, refresh_token: auth.refresh_token
json = JSON.parse(response.body)
expect(response.body).to include "expires_in"
@ -100,15 +104,15 @@ describe OpenidConnect::TokenEndpoint, type: :request do
context "when the refresh token is not valid" do
it "should return an invalid grant error" do
post "/openid_connect/access_tokens", grant_type: "refresh_token",
client_id: client.client_id, client_secret: client.client_secret, refresh_token: " "
post openid_connect_access_tokens_path, grant_type: "refresh_token",
client_id: client.client_id, client_secret: client.client_secret, refresh_token: "123456"
expect(response.body).to include "invalid_grant"
end
end
context "when the client is unregistered" do
it "should return an error" do
post "/openid_connect/access_tokens", grant_type: "refresh_token", refresh_token: auth.refresh_token,
post openid_connect_access_tokens_path, grant_type: "refresh_token", refresh_token: auth.refresh_token,
client_id: SecureRandom.hex(16).to_s, client_secret: client.client_secret
expect(response.body).to include "invalid_client"
end
@ -116,7 +120,7 @@ describe OpenidConnect::TokenEndpoint, type: :request do
context "when the refresh_token field is missing" do
it "should return an invalid request error" do
post "/openid_connect/access_tokens", grant_type: "refresh_token",
post openid_connect_access_tokens_path, grant_type: "refresh_token",
client_id: client.client_id, client_secret: client.client_secret
expect(response.body).to include "'refresh_token' required"
end
@ -124,7 +128,7 @@ describe OpenidConnect::TokenEndpoint, type: :request do
context "when the client_secret doesn't match" do
it "should return an invalid client error" do
post "/openid_connect/access_tokens", grant_type: "refresh_token", refresh_token: auth.refresh_token,
post openid_connect_access_tokens_path, grant_type: "refresh_token", refresh_token: auth.refresh_token,
client_id: client.client_id, client_secret: "client.client_secret"
expect(response.body).to include "invalid_client"
end