From ee9ac06e1a6c1d6cdccaee15d758e0b90e899df7 Mon Sep 17 00:00:00 2001
From: theworldbright <kent@kentshikama.com>
Date: Thu, 16 Jul 2015 01:53:10 +0900
Subject: [PATCH] Add support for access tokens in implicit flow

Squashed commits:
[7dbf618] Use Rail's find_or_create_by method
---
 .../authorizations_controller.rb              |  2 +-
 app/models/openid_connect/authorization.rb    | 21 ++---
 .../openid_connect/o_auth_access_token.rb     |  2 +-
 .../openid_connect/o_auth_application.rb      |  2 +-
 .../endpoint_confirmation_point.rb            | 13 ++--
 .../authorizations_controller_spec.rb         | 76 +++++++++++++------
 6 files changed, 73 insertions(+), 43 deletions(-)

diff --git a/app/controllers/openid_connect/authorizations_controller.rb b/app/controllers/openid_connect/authorizations_controller.rb
index 3d4a55eb94..febf6247a5 100644
--- a/app/controllers/openid_connect/authorizations_controller.rb
+++ b/app/controllers/openid_connect/authorizations_controller.rb
@@ -76,7 +76,7 @@ class OpenidConnect::AuthorizationsController < ApplicationController
     req = Rack::Request.new(request.env)
     req.update_param("client_id", session[:client_id])
     req.update_param("redirect_uri", session[:redirect_uri])
-    req.update_param("response_type", session[:response_type])
+    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("request_object", session[:request_object])
     req.update_param("nonce", session[:nonce])
diff --git a/app/models/openid_connect/authorization.rb b/app/models/openid_connect/authorization.rb
index 6793c59ce4..c91aa2cc66 100644
--- a/app/models/openid_connect/authorization.rb
+++ b/app/models/openid_connect/authorization.rb
@@ -2,12 +2,12 @@ class OpenidConnect::Authorization < ActiveRecord::Base
   belongs_to :user
   belongs_to :o_auth_application
 
-  validates :user, presence: true, uniqueness: true
-  validates :o_auth_application, presence: true, uniqueness: true
+  validates :user, presence: true
+  validates :o_auth_application, presence: true
 
   has_many :scopes, through: :authorization_scopes
   has_many :o_auth_access_tokens, dependent: :destroy
-  has_many :id_tokens
+  has_many :id_tokens, dependent: :destroy
 
   def generate_refresh_token
     self.refresh_token = SecureRandom.hex(32)
@@ -15,21 +15,16 @@ class OpenidConnect::Authorization < ActiveRecord::Base
 
   def create_access_token
     o_auth_access_tokens.create!.bearer_token
+    # TODO: Add support for request object
   end
 
-  def self.find_by_client_id_and_user(client_id, user)
-    app = OpenidConnect::OAuthApplication.find_by(client_id: client_id)
-    find_by(o_auth_application: app, user: user)
-  end
-
-  def self.find_by_app_and_user(app, user)
-    find_by(o_auth_application: app, user: user)
+  def create_id_token(nonce)
+    id_tokens.create!(nonce: nonce)
   end
 
-  # TODO: Handle creation error
-  def self.find_or_create(client_id, user)
+  def self.find_by_client_id_and_user(client_id, user)
     app = OpenidConnect::OAuthApplication.find_by(client_id: client_id)
-    find_by_app_and_user(app, user) || create!(user: user, o_auth_application: app)
+    find_by(o_auth_application: app, user: user)
   end
 
   # TODO: Consider splitting into subclasses by flow type
diff --git a/app/models/openid_connect/o_auth_access_token.rb b/app/models/openid_connect/o_auth_access_token.rb
index de46e837f9..04f423f4de 100644
--- a/app/models/openid_connect/o_auth_access_token.rb
+++ b/app/models/openid_connect/o_auth_access_token.rb
@@ -5,7 +5,7 @@ class OpenidConnect::OAuthAccessToken < ActiveRecord::Base
   before_validation :setup, on: :create
 
   validates :token, presence: true, uniqueness: true
-  validates :authorization, presence: true, uniqueness: true
+  validates :authorization, presence: true
 
   scope :valid, ->(time) { where("expires_at >= ?", time) }
 
diff --git a/app/models/openid_connect/o_auth_application.rb b/app/models/openid_connect/o_auth_application.rb
index 2ba85ba0b4..482f0ad249 100644
--- a/app/models/openid_connect/o_auth_application.rb
+++ b/app/models/openid_connect/o_auth_application.rb
@@ -16,7 +16,7 @@ class OpenidConnect::OAuthApplication < ActiveRecord::Base
 
   class << self
     def available_response_types
-      ["id_token"]
+      ["id_token", "id_token token"]
     end
 
     def register!(registrar)
diff --git a/lib/openid_connect/authorization_point/endpoint_confirmation_point.rb b/lib/openid_connect/authorization_point/endpoint_confirmation_point.rb
index 7b91d0123c..cdb4d94742 100644
--- a/lib/openid_connect/authorization_point/endpoint_confirmation_point.rb
+++ b/lib/openid_connect/authorization_point/endpoint_confirmation_point.rb
@@ -18,14 +18,17 @@ module OpenidConnect
         end
       end
 
+      # TODO: Add support for request object and auth code
       def approved!(req, res)
-        auth = OpenidConnect::Authorization.find_or_create(req.client_id, @user)
+        auth = OpenidConnect::Authorization.find_or_create_by(o_auth_application: @o_auth_application, user: @user)
         response_types = Array(req.response_type)
+        if response_types.include?(:token)
+          res.access_token = auth.create_access_token
+        end
         if response_types.include?(:id_token)
-          id_token = auth.id_tokens.create!(nonce: req.nonce)
-          options = %i(code access_token).map{|option| ["res.#{option}", res.respond_to?(option) ? res.option : nil]}.to_h
-          res.id_token = id_token.to_jwt(options)
-          # TODO: Add support for request object
+          id_token = auth.create_id_token(req.nonce)
+          access_token_value = res.respond_to?(:access_token) ? res.access_token : nil
+          res.id_token = id_token.to_jwt(code: nil, access_token: access_token_value)
         end
         res.approve!
       end
diff --git a/spec/controllers/openid_connect/authorizations_controller_spec.rb b/spec/controllers/openid_connect/authorizations_controller_spec.rb
index 08b65bc173..bb597254db 100644
--- a/spec/controllers/openid_connect/authorizations_controller_spec.rb
+++ b/spec/controllers/openid_connect/authorizations_controller_spec.rb
@@ -89,7 +89,7 @@ describe OpenidConnect::AuthorizationsController, type: :controller do
       end
     end
     context "when already authorized" do
-      let!(:auth) { OpenidConnect::Authorization.find_or_create(client.client_id, alice) }
+      let!(:auth) { OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: alice) }
 
       context "when valid parameters are passed" do
         before do
@@ -113,41 +113,73 @@ describe OpenidConnect::AuthorizationsController, type: :controller do
   end
 
   describe "#create" do
-    before do
-      get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
-          scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
-    end
 
-    context "when authorization is approved" do
+    context "when id_token token" do
       before do
-        post :create, approve: "true"
+        get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token token",
+            scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
       end
 
-      it "should return the id token in a fragment" do
-        expect(response.location).to have_content("id_token=")
-        encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
-        decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, OpenidConnect::IdTokenConfig.public_key
-        expect(decoded_token.nonce).to eq("4180930983")
-        expect(decoded_token.exp).to be > Time.now.utc.to_i
-      end
+      context "when authorization is approved" do
+        before do
+          post :create, approve: "true"
+        end
+
+        it "should return the id token in a fragment" do
+          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
+          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, OpenidConnect::IdTokenConfig.public_key
+          expect(decoded_token.nonce).to eq("4180930983")
+          expect(decoded_token.exp).to be > Time.now.utc.to_i
+        end
 
-      it "should return the passed in state" do
-        expect(response.location).to have_content("state=4180930983")
+        it "should return a valid access token in a fragment" do
+          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
+          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, OpenidConnect::IdTokenConfig.public_key
+          access_token = response.location[/(?<=access_token=)[^&]+/]
+          access_token_check_num = UrlSafeBase64.encode64(OpenSSL::Digest::SHA256.digest(access_token)[0, 128 / 8])
+          expect(decoded_token.at_hash).to eq(access_token_check_num)
+        end
       end
     end
 
-    context "when authorization is denied" do
+    context "when id_token" do
       before do
-        post :create, approve: "false"
+        get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
+            scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
       end
 
-      it "should return an error in the fragment" do
-        expect(response.location).to have_content("error=")
+      context "when authorization is approved" do
+        before do
+          post :create, approve: "true"
+        end
+
+        it "should return the id token in a fragment" do
+          expect(response.location).to have_content("id_token=")
+          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
+          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, OpenidConnect::IdTokenConfig.public_key
+          expect(decoded_token.nonce).to eq("4180930983")
+          expect(decoded_token.exp).to be > Time.now.utc.to_i
+        end
+
+        it "should return the passed in state" do
+          expect(response.location).to have_content("state=4180930983")
+        end
       end
 
-      it "should NOT contain a id token in the fragment" do
-        expect(response.location).to_not have_content("id_token=")
+      context "when authorization is denied" do
+        before do
+          post :create, approve: "false"
+        end
+
+        it "should return an error in the fragment" do
+          expect(response.location).to have_content("error=")
+        end
+
+        it "should NOT contain a id token in the fragment" do
+          expect(response.location).to_not have_content("id_token=")
+        end
       end
     end
+
   end
 end
-- 
GitLab