-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
300 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
require "../spec_helper" | ||
|
||
describe MultiAuth do | ||
context "vk" do | ||
it "generates authorize_uri" do | ||
uri = MultiAuth.make("vk", "/callback").authorize_uri | ||
uri.should eq("https://oauth.vk.com/authorize?client_id=vk_id&redirect_uri=%2Fcallback&response_type=code&scope=email") | ||
end | ||
|
||
it "fetch user" do | ||
WebMock.wrap do | ||
WebMock | ||
.stub(:post, "https://oauth.vk.com/access_token") | ||
.with( | ||
body: "client_id=vk_id&client_secret=vk_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123", | ||
headers: {"Accept" => "application/json", "Content-Length" => "103", "Host" => "oauth.vk.com", "Content-type" => "application/x-www-form-urlencoded"}) | ||
.to_return( | ||
body: %({ | ||
"access_token" : "1111", | ||
"expires_in" : 899, | ||
"refresh_token" : null, | ||
"scope" : "email", | ||
"user_id" : "3333", | ||
"email" : "[email protected]" | ||
}) | ||
) | ||
|
||
WebMock | ||
.stub(:get, %(https://api.vk.com/method/users.get?fields=about,photo_max_orig,city,country,domain,contacts,site&user_id="3333"&v=5.52)) | ||
.to_return( | ||
body: %({"response": [{ | ||
"first_name" : "Sergey", | ||
"last_name" : "Makridenkov", | ||
"id" : 3333 | ||
}]}) | ||
) | ||
|
||
user = MultiAuth.make("vk", "/callback").user({"code" => "123"}).as(MultiAuth::User) | ||
|
||
user.name.should eq("Makridenkov Sergey") | ||
user.uid.should eq("3333") | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# TODO remove hack after release | ||
# https://github.com/crystal-lang/crystal/commit/31099237c87a3851c8cb2a78df5ff00bac7364c6 | ||
|
||
# Base class for the two possible access tokens: Bearer and Mac. | ||
# | ||
# Use `#authenticate` to authenticate an `HTTP::Client`. | ||
abstract class OAuth2::AccessToken | ||
def self.new(pull : JSON::PullParser) | ||
token_type = nil | ||
access_token = nil | ||
expires_in = nil | ||
refresh_token = nil | ||
scope = nil | ||
mac_algorithm = nil | ||
mac_key = nil | ||
extra = nil | ||
|
||
pull.read_object do |key| | ||
case key | ||
when "token_type" then token_type = pull.read_string | ||
when "access_token" then access_token = pull.read_string | ||
when "expires_in" then expires_in = pull.read_int | ||
when "refresh_token" then refresh_token = pull.read_string_or_null | ||
when "scope" then scope = pull.read_string_or_null | ||
when "mac_algorithm" then mac_algorithm = pull.read_string | ||
when "mac_key" then mac_key = pull.read_string | ||
else | ||
extra ||= {} of String => String | ||
extra[key] = pull.read_raw | ||
end | ||
end | ||
|
||
access_token = access_token.not_nil! | ||
token_type ||= "bearer" | ||
|
||
case token_type.downcase | ||
when "bearer" | ||
Bearer.new(access_token, expires_in, refresh_token, scope, extra) | ||
when "mac" | ||
Mac.new(access_token, expires_in, mac_algorithm.not_nil!, mac_key.not_nil!, refresh_token, scope, Time.now.epoch, extra) | ||
else | ||
raise "Uknown token_type in access token json: #{token_type}" | ||
end | ||
end | ||
|
||
property access_token : String | ||
property expires_in : Int64? | ||
property refresh_token : String? | ||
property scope : String? | ||
|
||
# JSON key-value pairs that are outside of the OAuth2 spec are | ||
# stored in this property in case they are needed. Their value | ||
# is the raw JSON string found in the JSON value (with possible | ||
# changes in the string format, but preserving JSON semantic). | ||
# For example if the value was `[1, 2, 3]` then the value in this hash | ||
# will be the string "[1,2,3]". | ||
property extra : Hash(String, String)? | ||
|
||
def initialize(@access_token : String, expires_in : Int?, @refresh_token : String? = nil, @scope : String? = nil, @extra = nil) | ||
@expires_in = expires_in.try &.to_i64 | ||
end | ||
|
||
abstract def authenticate(request : HTTP::Request, tls) | ||
|
||
def authenticate(client : HTTP::Client) | ||
client.before_request do |request| | ||
authenticate request, client.tls? | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# TODO remove hack after release | ||
# https://github.com/crystal-lang/crystal/commit/31099237c87a3851c8cb2a78df5ff00bac7364c6 | ||
|
||
require "secure_random" | ||
require "openssl/hmac" | ||
require "base64" | ||
require "./access_token" | ||
|
||
class OAuth2::AccessToken::Mac < OAuth2::AccessToken | ||
def self.new(pull : JSON::PullParser) | ||
OAuth2::AccessToken.new(pull).as(self) | ||
end | ||
|
||
property mac_algorithm : String | ||
property mac_key : String | ||
property issued_at : Int64 | ||
|
||
def initialize(access_token, expires_in, @mac_algorithm, @mac_key, refresh_token = nil, scope = nil, @issued_at = Time.now.epoch, extra = nil) | ||
super(access_token, expires_in, refresh_token, scope, extra) | ||
end | ||
|
||
def token_type | ||
"Mac" | ||
end | ||
|
||
def authenticate(request : HTTP::Request, tls) | ||
ts = Time.now.epoch | ||
nonce = "#{ts - @issued_at}:#{SecureRandom.hex}" | ||
method = request.method | ||
uri = request.resource | ||
host, port = host_and_port request, tls | ||
ext = "" | ||
|
||
mac = Mac.signature ts, nonce, method, uri, host, port, ext, mac_algorithm, mac_key | ||
|
||
header = %(MAC id="#{access_token}", nonce="#{nonce}", ts="#{ts}", mac="#{mac}") | ||
request.headers["Authorization"] = header | ||
end | ||
|
||
def self.signature(ts, nonce, method, uri, host, port, ext, mac_algorithm, mac_key) | ||
normalized_request_string = "#{ts}\n#{nonce}\n#{method}\n#{uri}\n#{host}\n#{port}\n#{ext}\n" | ||
|
||
digest = case mac_algorithm | ||
when "hmac-sha-1" then :sha1 | ||
when "hmac-sha-256" then :sha256 | ||
else raise "Unsupported algorithm: #{mac_algorithm}" | ||
end | ||
Base64.strict_encode OpenSSL::HMAC.digest(digest, mac_key, normalized_request_string) | ||
end | ||
|
||
def to_json(json : JSON::Builder) | ||
json.object do | ||
json.field "token_type", "mac" | ||
json.field "access_token", access_token | ||
json.field "expires_in", expires_in | ||
json.field "refresh_token", refresh_token if refresh_token | ||
json.field "scope", scope if scope | ||
json.field "mac_algorithm", mac_algorithm | ||
json.field "mac_key", mac_key | ||
end | ||
end | ||
|
||
def_equals_and_hash access_token, expires_in, mac_algorithm, mac_key, refresh_token, scope | ||
|
||
private def host_and_port(request, tls) | ||
host_header = request.headers["Host"] | ||
if colon_index = host_header.index ':' | ||
host = host_header[0...colon_index] | ||
port = host_header[colon_index + 1..-1].to_i | ||
else | ||
host = host_header | ||
port = tls ? 443 : 80 | ||
end | ||
{host, port} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
require "oauth2" | ||
require "./crystal_std_patch/**" | ||
require "./multi_auth/**" | ||
|
||
module MultiAuth | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
class MultiAuth::Provider::Vk < MultiAuth::Provider | ||
def authorize_uri(scope = nil) | ||
@scope = scope || "email" | ||
client.get_authorize_uri(@scope) | ||
end | ||
|
||
def user(params : Hash(String, String)) | ||
vk_user = fetch_vk_user(params["code"]) | ||
|
||
user = User.new("vk", vk_user.id, vk_user.name, vk_user.raw_json.not_nil!) | ||
|
||
user.email = vk_user.email | ||
user.first_name = vk_user.first_name | ||
user.last_name = vk_user.last_name | ||
user.nickname = vk_user.domain | ||
user.description = vk_user.about | ||
user.image = vk_user.photo_max_orig | ||
user.phone = vk_user.mobile_phone || vk_user.home_phone | ||
user.access_token = vk_user.access_token | ||
|
||
location = [] of String | ||
location << vk_user.city.not_nil!.title if vk_user.city | ||
location << vk_user.country.not_nil!.title if vk_user.country | ||
user.location = location.join(", ") unless location.empty? | ||
|
||
urls = {} of String => String | ||
urls["web"] = vk_user.site.not_nil! if vk_user.site | ||
user.urls = urls unless urls.empty? | ||
|
||
user | ||
end | ||
|
||
class VkTitle | ||
JSON.mapping( | ||
title: String | ||
) | ||
end | ||
|
||
class VkUser | ||
property raw_json : String? | ||
property access_token : OAuth2::AccessToken? | ||
property email : String? | ||
property id : String? | ||
|
||
def name | ||
"#{last_name} #{first_name}" | ||
end | ||
|
||
JSON.mapping( | ||
id: {type: String, converter: String::RawConverter}, | ||
last_name: String?, | ||
first_name: String?, | ||
site: String?, | ||
city: VkTitle?, | ||
country: VkTitle?, | ||
domain: String?, | ||
about: String?, | ||
photo_max_orig: String?, | ||
mobile_phone: String?, | ||
home_phone: String? | ||
) | ||
end | ||
|
||
class VkResponse | ||
JSON.mapping( | ||
response: Array(VkUser), | ||
) | ||
end | ||
|
||
private def fetch_vk_user(code) | ||
access_token = client.get_access_token_using_authorization_code(code) | ||
|
||
api = HTTP::Client.new("api.vk.com", tls: true) | ||
access_token.authenticate(api) | ||
|
||
user_id = access_token.extra.not_nil!["user_id"] | ||
user_email = access_token.extra.not_nil!["email"] | ||
|
||
fields = "about,photo_max_orig,city,country,domain,contacts,site" | ||
raw_json = api.get("/method/users.get?fields=#{fields}&user_id=#{user_id}&v=5.52").body | ||
|
||
vk_user = VkResponse.from_json(raw_json).response.first | ||
vk_user.email = user_email | ||
vk_user.access_token = access_token | ||
vk_user.raw_json = raw_json | ||
|
||
vk_user | ||
end | ||
|
||
private def client | ||
OAuth2::Client.new( | ||
"oauth.vk.com", | ||
client_id, | ||
client_secret, | ||
redirect_uri: redirect_uri, | ||
authorize_uri: "/authorize", | ||
token_uri: "/access_token" | ||
) | ||
end | ||
end |