├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── certs └── Eric-Guo.pem ├── lib ├── omniauth-wechat-oauth2.rb └── omniauth │ ├── strategies │ ├── wechat.rb │ └── wechat_qiye.rb │ └── wechat.rb ├── omniauth-wechat-oauth2.gemspec └── spec ├── omniauth └── strategies │ ├── wechat_qiye_spec.rb │ └── wechat_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .ruby-version 6 | .ruby-gemset 7 | Gemfile.lock 8 | coverage 9 | InstalledFiles 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | 18 | # YARD artifacts 19 | .yardoc 20 | _yardoc 21 | doc/ 22 | .byebug_history 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - gem install bundler -v 1.17.3 3 | 4 | language: ruby 5 | 6 | env: 7 | global: 8 | - NOKOGIRI_USE_SYSTEM_LIBRARIES=true 9 | 10 | sudo: false 11 | 12 | rvm: 13 | - 2.4.10 14 | - 2.5.8 15 | - 2.6.6 16 | - 2.7.1 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | 4 | 5 | gem "rake", ">= 12.3.3" 6 | gem 'byebug' 7 | 8 | gemspec 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 skinnyworm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Omniauth-wechat-oauth2 2 | ====================== 3 | 4 | [![Gem Version](https://img.shields.io/gem/v/omniauth-wechat-oauth2.svg)][gem] 5 | [![Security Check](https://hakiri.io/github/NeverMin/omniauth-wechat-oauth2/master.svg)][security] 6 | [![Build Status](https://app.travis-ci.com/Eric-Guo/omniauth-wechat-oauth2.svg?branch=master)][travis] 7 | 8 | [gem]: https://rubygems.org/gems/omniauth-wechat-oauth2 9 | [security]: https://hakiri.io/github/NeverMin/omniauth-wechat-oauth2/master 10 | [travis]: https://app.travis-ci.com/github/Eric-Guo/omniauth-wechat-oauth2 11 | 12 | 13 | Wechat OAuth2 Strategy for OmniAuth 1.0. 14 | 15 | You need to get a wechat API key at: https://mp.weixin.qq.com 16 | 17 | * Wechat oauth2 specification can be found at: https://mp.weixin.qq.com/wiki/index.php?title=网页授权获取用户基本信息 18 | * Wechat Qiye oauth2 specification can be found at: http://qydev.weixin.qq.com/wiki/index.php?title=OAuth验证接口 19 | 20 | ## Installation 21 | 22 | Add to your `Gemfile`: 23 | 24 | ```ruby 25 | gem "omniauth-wechat-oauth2" 26 | ``` 27 | 28 | Then `bundle install`. 29 | 30 | 31 | ## Usage 32 | 33 | Here's an example for adding the middleware to a Rails app in `config/initializers/omniauth.rb`: 34 | 35 | ```ruby 36 | Rails.application.config.middleware.use OmniAuth::Builder do 37 | provider :wechat, ENV["WECHAT_APP_ID"], ENV["WECHAT_APP_SECRET"] 38 | end 39 | ``` 40 | 41 | You can now access the OmniAuth Wechat OAuth2 URL: `/auth/wechat` 42 | 43 | ## Configuration 44 | 45 | You can configure several options, which you pass in to the `provider` method via a hash: 46 | 47 | * `scope`: Default is "snsapi_userinfo". It can either be *snsapi_base* or *snsapi_userinfo*. When scope is "snsapi_userinfo", after wechat user is authenticated, app can query userinfo using the acquired access_token. 48 | 49 | For devise user, you can set up scope in your devise.rb as following. 50 | 51 | ```ruby 52 | config.omniauth :wechat, ENV["WECHAT_APP_ID"], ENV["WECHAT_APP_SECRET"], 53 | :authorize_params => {:scope => "snsapi_base"} 54 | ``` 55 | 56 | ## Auth Hash 57 | 58 | Here's an example of an authentication hash available in the callback by accessing `request.env["omniauth.auth"]`: 59 | 60 | ```ruby 61 | { 62 | :provider => "wechat", 63 | :uid => "123456789", 64 | :info => { 65 | nickname: "Nickname", 66 | sex: 1, 67 | province: "Changning", 68 | city: "Shanghai", 69 | country: "China", 70 | headimgurl: "http://image_url", 71 | unionid: 'unionid' 72 | }, 73 | :credentials => { 74 | :token => "token", 75 | :refresh_token => "another_token", 76 | :expires_at => 7200, 77 | :expires => true 78 | }, 79 | :extra => { 80 | :raw_info => { 81 | openid: "openid" 82 | nickname: "Nickname", 83 | sex: 1, 84 | province: "Changning", 85 | city: "Shanghai", 86 | country: "China", 87 | headimgurl: "http://image_url", 88 | unionid: 'unionid' 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | ## Wechat Qiye OAuth2 95 | 96 | Wechat Qiey usage and configuration are the same with normal account above. 97 | 98 | ```ruby 99 | config.omniauth :wechat_qiye, ENV["WECHAT_APP_ID"], ENV["WECHAT_APP_SECRET"], 100 | :authorize_params => {:scope => "snsapi_base"} 101 | ``` 102 | 103 | Auth hash `request.env["omniauth.auth"]` 104 | 105 | ```ruby 106 | { 107 | :provider => "wechat_qiye", 108 | :uid => "123456789", 109 | :info => { 110 | userid: "userid", 111 | name: "name", 112 | department: [2], 113 | gender: "1", 114 | weixinid: "weixinid", 115 | avatar: "avatar", 116 | status: 1, 117 | extattr: {"foo" => "bar"} 118 | }, 119 | :credentials => { 120 | :token => "token", 121 | :refresh_token => "another_token", 122 | :expires_at => 7200, 123 | :expires => true 124 | }, 125 | :extra => { 126 | :raw_info => { 127 | userid: "userid", 128 | name: "name", 129 | department: [2], 130 | gender: "1", 131 | weixinid: "weixinid", 132 | avatar: "avatar", 133 | status: 1, 134 | extattr: {"foo" => "bar"}} 135 | } 136 | } 137 | } 138 | ``` 139 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require File.join('bundler', 'gem_tasks') 3 | require File.join('rspec', 'core', 'rake_task') 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | -------------------------------------------------------------------------------- /certs/Eric-Guo.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEeDCCAuCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMRMwEQYDVQQDDAplcmlj 3 | Lmd1b2N6MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZFgNj 4 | b20wHhcNMjIxMjA5MDcyMTE1WhcNMjMxMjA5MDcyMTE1WjBBMRMwEQYDVQQDDApl 5 | cmljLmd1b2N6MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ 6 | FgNjb20wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQC6jGarls5mU792 7 | leWot9tWKXnnvVnj1oUPea8M77PpHcYMVmjUTkjgsrKiOFNv2nAjUIDrUaLJvuKi 8 | 95zZYLIT3uWqI9QvSSDr+rs4HyIDcXuWWB89GpKN5WgyCVel6V9X5PN0C5hIiz8T 9 | ThW7ZqkS8mTmHm2micL8/lmnelpxj9EjCin7xOCvebB9pdA7Y0kXMLbnRq4QK2lB 10 | O2YRbLvYKWOQfO/sqLlwFsNWMNkj5ZROIqmKRvOHQdchox2RfBNsWRZ7XH8tTW1L 11 | cqx95dI2qC6Xg+IHjN4Wc6n+Ak1KENlVjI4mGcuk+34d1S9gaZT8vW/cYcL30Nbg 12 | AAUQvS4AQ8gmAkk1+oNs+725QLBprbnSN5PJr9VQiDteNHHkY5ugjndeAByOGy8h 13 | /TgV4tBZQ0IT37ZRDcP4dayFMm2DVjS4uU7RUXLgc8sdOGeMmsFwdz9QrBPGeKsV 14 | 4+dXjrzzUvwRX4K21mx4ZSP9eSOscOEuzRIRTnab0GniDhkkHc8CAwEAAaN7MHkw 15 | CQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFPGkToJNhoEfhsOYqnTH 16 | MZDYyjQtMB8GA1UdEQQYMBaBFGVyaWMuZ3VvY3pAZ21haWwuY29tMB8GA1UdEgQY 17 | MBaBFGVyaWMuZ3VvY3pAZ21haWwuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQCdTl5T 18 | qfcrbo4KdgZM84ZbeiUIXcRy/chbfQfPZ/qPp7qKARLxRJINmaqt/bP+RrlQypEe 19 | 1EIYhbxLGo1TvpbRFuADUhZjGZ8Vaa+bgPfj8Dxa71ow5dF9nrqidddV7qjmk57F 20 | klOrOl+99Ryx01OAdlxGZqz/VrHrzbhQNtiUl2BYnosZLddYPTzuhNLe72eIypkY 21 | synsOqoXRuegCtO4sSGSvRV79/GyN0jqtptpa61MaxgYG13+P8QwXeTo2Ro86ZAJ 22 | h+KBs65p0MiFHl6zc2oany9Pk0kqs50TDKOCT7ZdSxz5xes0SvTi3mmX3XemjQhm 23 | qNb+5zsj4Kn5auKYA7GupfLYKS/dYt4EIfNKhSEHgkVFdVPxSAQ73UAWsndINTTA 24 | HjK18j09PoL9vWMUtyez2xpxlRDA+bEkF74AEVwMYozjP8VNM1ERTG+8kROuQjGV 25 | aWhGk3yVuHjmlBvfDpPqmux3ulWXnvHulcYiPxETh/m3PUfIKMTHydjAz88= 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /lib/omniauth-wechat-oauth2.rb: -------------------------------------------------------------------------------- 1 | require "omniauth/wechat" 2 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/wechat.rb: -------------------------------------------------------------------------------- 1 | require "omniauth-oauth2" 2 | 3 | module OmniAuth 4 | module Strategies 5 | class Wechat < OmniAuth::Strategies::OAuth2 6 | option :name, "wechat" 7 | 8 | option :client_options, { 9 | site: "https://api.weixin.qq.com", 10 | authorize_url: "https://open.weixin.qq.com/connect/qrconnect?#wechat_redirect", 11 | token_url: "/sns/oauth2/access_token", 12 | token_method: :get 13 | } 14 | 15 | option :authorize_params, {scope: "snsapi_login"} 16 | 17 | option :token_params, {parse: :json} 18 | 19 | def callback_url 20 | full_host + script_name + callback_path 21 | end 22 | 23 | uid do 24 | raw_info['openid'] 25 | end 26 | 27 | info do 28 | { 29 | nickname: raw_info['nickname'], 30 | sex: raw_info['sex'], 31 | province: raw_info['province'], 32 | city: raw_info['city'], 33 | country: raw_info['country'], 34 | headimgurl: raw_info['headimgurl'], 35 | image: raw_info['headimgurl'], 36 | unionid: raw_info['unionid'] 37 | } 38 | end 39 | 40 | extra do 41 | {raw_info: raw_info} 42 | end 43 | 44 | def request_phase 45 | params = client.auth_code.authorize_params.merge(authorize_params) 46 | params["appid"] = params.delete("client_id") 47 | params["redirect_uri"] = callback_url 48 | redirect client.authorize_url(params) 49 | end 50 | 51 | def raw_info 52 | @uid ||= access_token["openid"] 53 | @raw_info ||= begin 54 | access_token.options[:mode] = :query 55 | if ["snsapi_login", "snsapi_userinfo"].include?(access_token["scope"]) 56 | access_token.get("/sns/userinfo", :params => { "openid" => @uid, "lang" => "zh_CN" }, parse: :json).parsed 57 | else 58 | { "openid" => @uid } 59 | end 60 | end 61 | @raw_info 62 | end 63 | 64 | protected 65 | def build_access_token 66 | params = { 67 | 'appid' => client.id, 68 | 'secret' => client.secret, 69 | 'code' => request.params['code'], 70 | 'grant_type' => 'authorization_code', 71 | 'redirect_uri' => callback_url 72 | }.merge(token_params.to_hash(symbolize_keys: true)) 73 | client.get_token(params, deep_symbolize(options.auth_token_params)) 74 | end 75 | 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/wechat_qiye.rb: -------------------------------------------------------------------------------- 1 | require "omniauth-oauth2" 2 | 3 | module OmniAuth 4 | module Strategies 5 | class WechatQiye < OmniAuth::Strategies::OAuth2 6 | option :name, "wechat_qiye" 7 | 8 | option :client_options, { 9 | :site => "https://qyapi.weixin.qq.com", 10 | authorize_url: "https://open.weixin.qq.com/connect/oauth2/authorize#wechat_redirect", 11 | token_url: "/cgi-bin/gettoken", 12 | token_method: :get, 13 | connection_opts: { 14 | ssl: { verify: false } 15 | } 16 | } 17 | 18 | option :authorize_params, {scope: "snsapi_userinfo"} 19 | option :token_params, {parse: :json} 20 | 21 | uid do 22 | raw_info['userid'] 23 | end 24 | 25 | info do 26 | { 27 | userid: raw_info['userid'], 28 | name: raw_info['name'], 29 | department: raw_info['department'], 30 | gender: raw_info['gender'], 31 | weixinid: raw_info['weixinid'], 32 | avatar: raw_info['avatar'], 33 | status: raw_info['status'], 34 | extattr: raw_info['extattr'] 35 | } 36 | end 37 | 38 | extra do 39 | { raw_info: raw_info } 40 | end 41 | 42 | def request_phase 43 | params = client.auth_code.authorize_params.merge(redirect_uri: callback_url).merge(authorize_params) 44 | params["appid"] = params.delete("client_id") 45 | redirect client.authorize_url(params) 46 | end 47 | 48 | def raw_info 49 | # step 2: get userid via code and access_token 50 | @code ||= access_token[:code] 51 | 52 | # step 3: get user info via userid 53 | @uid ||= begin 54 | access_token.options[:mode] = :query 55 | response = access_token.get('/cgi-bin/user/getuserinfo', :params => {'code' => @code}, parse: :json) 56 | response.parsed['UserId'] 57 | end 58 | 59 | @raw_info ||= begin 60 | access_token.options[:mode] = :query 61 | response = access_token.get("/cgi-bin/user/get", :params => {"userid" => @uid}, parse: :json) 62 | response.parsed 63 | end 64 | end 65 | 66 | protected 67 | def build_access_token 68 | # step 0: wechat respond code 69 | code = request.params['code'] 70 | 71 | # step 1: get access token 72 | params = { 73 | 'corpid' => client.id, 74 | 'corpsecret' => client.secret, 75 | }.merge(token_params.to_hash(symbolize_keys: true)) 76 | client.get_token(params, deep_symbolize(options.auth_token_params.merge({code: code}))) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/omniauth/wechat.rb: -------------------------------------------------------------------------------- 1 | require "omniauth/strategies/wechat" 2 | require "omniauth/strategies/wechat_qiye" 3 | -------------------------------------------------------------------------------- /omniauth-wechat-oauth2.gemspec: -------------------------------------------------------------------------------- 1 | version = File.read(File.expand_path('../VERSION', __FILE__)).strip 2 | 3 | Gem::Specification.new do |s| 4 | s.platform = Gem::Platform::RUBY 5 | s.name = 'omniauth-wechat-oauth2' 6 | s.version = version 7 | s.summary = 'Omniauth strategy for wechat(weixin)' 8 | s.description = 'Using OAuth2 to authenticate wechat user when web resources being viewed within wechat(weixin) client.' 9 | 10 | s.files = Dir['README.md', 'lib/**/*'] 11 | s.require_path = 'lib' 12 | s.requirements << 'none' 13 | s.required_ruby_version = '>= 2.4.0' 14 | 15 | s.cert_chain = ['certs/Eric-Guo.pem'] 16 | s.signing_key = File.expand_path('~/.ssh/gem-private_key.pem') if $PROGRAM_NAME.end_with?('gem') 17 | 18 | s.author = ['Alex Hu', 'Never Min', 'Eric Guo'] 19 | s.email = ['askinnyworm@gmail.com', 'Never.Min@gmail.com', 'eric@cloud-mes.com'] 20 | s.homepage = 'https://github.com/nevermin/omniauth-wechat-oauth2' 21 | s.license = 'MIT' 22 | 23 | s.add_dependency 'omniauth-oauth2', '>= 1.8.0' 24 | s.add_development_dependency 'rspec', '~> 3.10.0' 25 | end 26 | -------------------------------------------------------------------------------- /spec/omniauth/strategies/wechat_qiye_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe OmniAuth::Strategies::WechatQiye do 4 | let(:request) { double('Request', :params => {}, :cookies => {}, :env => {}, :scheme=>"http", :url=>"localhost") } 5 | let(:app) { ->{[200, {}, ["Hello."]]}} 6 | let(:client){OAuth2::Client.new('corpid', 'corpsecret')} 7 | 8 | subject do 9 | OmniAuth::Strategies::WechatQiye.new(app, 'corpid', 'corpsecret', @options || {}).tap do |strategy| 10 | allow(strategy).to receive(:request) { 11 | request 12 | } 13 | end 14 | end 15 | 16 | before do 17 | OmniAuth.config.test_mode = true 18 | end 19 | 20 | after do 21 | OmniAuth.config.test_mode = false 22 | end 23 | 24 | describe '#client_options' do 25 | specify 'has site' do 26 | expect(subject.client.site).to eq('https://qyapi.weixin.qq.com') 27 | end 28 | 29 | specify 'has authorize_url' do 30 | expect(subject.client.options[:authorize_url]).to eq('https://open.weixin.qq.com/connect/oauth2/authorize#wechat_redirect') 31 | end 32 | 33 | specify 'has token_url' do 34 | expect(subject.client.options[:token_url]).to eq('/cgi-bin/gettoken') 35 | end 36 | end 37 | 38 | describe "#authorize_params" do 39 | specify "default scope is snsapi_userinfo" do 40 | expect(subject.authorize_params[:scope]).to eq("snsapi_userinfo") 41 | end 42 | end 43 | 44 | describe "#token_params" do 45 | specify "token response should be parsed as json" do 46 | expect(subject.token_params[:parse]).to eq(:json) 47 | end 48 | end 49 | 50 | describe 'state' do 51 | specify 'should set state params for request as a way to verify CSRF' do 52 | expect(subject.authorize_params['state']).not_to be_nil 53 | expect(subject.authorize_params['state']).to eq(subject.session['omniauth.state']) 54 | end 55 | end 56 | 57 | describe "#request_phase" do 58 | specify "redirect uri includes 'appid', 'redirect_uri', 'response_type', 'scope', 'state' and 'wechat_redirect' fragment " do 59 | callback_url = "http://exammple.com/callback" 60 | 61 | subject.stub(:callback_url=>callback_url) 62 | expect(subject).to receive(:redirect) do |redirect_url| 63 | uri = URI.parse(redirect_url) 64 | expect(uri.fragment).to eq("wechat_redirect") 65 | params = CGI::parse(uri.query) 66 | expect(params["appid"]).to eq(['corpid']) 67 | expect(params["redirect_uri"]).to eq([callback_url]) 68 | expect(params["response_type"]).to eq(['code']) 69 | expect(params["scope"]).to eq(['snsapi_userinfo']) 70 | expect(params["state"]).to eq([subject.session['omniauth.state']]) 71 | end 72 | 73 | subject.request_phase 74 | end 75 | end 76 | 77 | describe "#build_access_token" do 78 | specify "request includes 'corpid', 'corpsecret' and will parse response as json"do 79 | subject.stub(:client => client, :request=>double("request", params:{ "code" => "server_code"})) 80 | client.should_receive(:get_token).with({ 81 | "corpid" => "corpid", 82 | "corpsecret" => "corpsecret", 83 | :parse => :json 84 | },{:code => "server_code"}) 85 | subject.send(:build_access_token) 86 | end 87 | end 88 | 89 | describe "#raw_info" do 90 | let(:access_token) { OAuth2::AccessToken.from_hash(client, { 91 | "expires_in"=>"expires_in", 92 | "access_token"=>"access_token", 93 | :code => "server_code" 94 | })} 95 | before { subject.stub(:access_token => access_token) } 96 | 97 | specify "will query for user info" do 98 | response_hash = { 99 | "userid" => "USERID", 100 | "name" => "NAME", 101 | "department" => [2], 102 | "gender" => "1", 103 | "weixinid" => "WXID", 104 | "avatar" => "AVATAR", 105 | "status" => "STATUS", 106 | "extattr" => {"foo" => "bar"} 107 | } 108 | 109 | userid_response = double("response", body: {"UserId" => 'USERID'}.to_json) 110 | userid_response.stub(:parsed).and_return({"UserId" => 'USERID'}) 111 | 112 | userinfo_params = {params: {"code"=> "server_code", "access_token"=> "access_token"}, parse: :json} 113 | client.should_receive(:request).with(:get, "/cgi-bin/user/getuserinfo", userinfo_params) 114 | .and_return(userid_response) 115 | 116 | userinfo_response = double("response", body: response_hash.to_json) 117 | (userinfo_response).stub(:parsed).and_return(response_hash) 118 | 119 | get_params = {params: { "userid"=> "USERID", "access_token"=> "access_token" }, parse: :json} 120 | client.should_receive(:request).with(:get, "/cgi-bin/user/get", get_params) 121 | .and_return(userinfo_response) 122 | 123 | expect(subject.uid).to eq("USERID") 124 | expect(subject.raw_info).to eq(response_hash) 125 | expect(subject.extra).to eq(raw_info: subject.raw_info) 126 | end 127 | 128 | end 129 | 130 | end 131 | -------------------------------------------------------------------------------- /spec/omniauth/strategies/wechat_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe OmniAuth::Strategies::Wechat do 4 | let(:request) { double('Request', :params => {}, :cookies => {}, :env => {}, :scheme=>"http", :url=>"localhost") } 5 | let(:app) { ->{[200, {}, ["Hello."]]}} 6 | let(:client){OAuth2::Client.new('appid', 'secret')} 7 | 8 | subject do 9 | OmniAuth::Strategies::Wechat.new(app, 'appid', 'secret', @options || {}).tap do |strategy| 10 | allow(strategy).to receive(:request) { 11 | request 12 | } 13 | end 14 | end 15 | 16 | before do 17 | OmniAuth.config.test_mode = true 18 | end 19 | 20 | after do 21 | OmniAuth.config.test_mode = false 22 | end 23 | 24 | describe '#client_options' do 25 | specify 'has site' do 26 | expect(subject.client.site).to eq('https://api.weixin.qq.com') 27 | end 28 | 29 | specify 'has authorize_url' do 30 | expect(subject.client.options[:authorize_url]).to eq('https://open.weixin.qq.com/connect/qrconnect?#wechat_redirect') 31 | end 32 | 33 | specify 'has token_url' do 34 | expect(subject.client.options[:token_url]).to eq('/sns/oauth2/access_token') 35 | end 36 | end 37 | 38 | describe "#authorize_params" do 39 | specify "default scope is snsapi_login" do 40 | expect(subject.authorize_params[:scope]).to eq("snsapi_login") 41 | end 42 | end 43 | 44 | describe "#token_params" do 45 | specify "token response should be parsed as json" do 46 | expect(subject.token_params[:parse]).to eq(:json) 47 | end 48 | end 49 | 50 | describe 'state' do 51 | specify 'should set state params for request as a way to verify CSRF' do 52 | expect(subject.authorize_params['state']).not_to be_nil 53 | expect(subject.authorize_params['state']).to eq(subject.session['omniauth.state']) 54 | end 55 | end 56 | 57 | 58 | describe "#request_phase" do 59 | specify "redirect uri includes 'appid', 'redirect_uri', 'response_type', 'scope', 'state' and 'wechat_redirect' fragment " do 60 | callback_url = "http://exammple.com/callback" 61 | 62 | subject.stub(:callback_url=>callback_url) 63 | expect(subject).to receive(:redirect) do |redirect_url| 64 | uri = URI.parse(redirect_url) 65 | expect(uri.fragment).to eq("wechat_redirect") 66 | params = CGI::parse(uri.query) 67 | expect(params["appid"]).to eq(['appid']) 68 | expect(params["redirect_uri"]).to eq([callback_url]) 69 | expect(params["response_type"]).to eq(['code']) 70 | expect(params["scope"]).to eq(['snsapi_login']) 71 | expect(params["state"]).to eq([subject.session['omniauth.state']]) 72 | end 73 | 74 | subject.request_phase 75 | end 76 | end 77 | 78 | describe "#build_access_token" do 79 | specify "request includes 'appid', 'secret', 'code', 'grant_type' and will parse response as json" do 80 | callback_url = "http://exammple.com/callback" 81 | subject.stub(:callback_url=>callback_url) 82 | 83 | subject.stub(:client => client, :request=>double("request", params:{"code"=>"server_code"})) 84 | client.should_receive(:get_token).with({ 85 | "appid" => "appid", 86 | "secret" => "secret", 87 | "code" => "server_code", 88 | "grant_type" => "authorization_code", 89 | "redirect_uri" => callback_url, 90 | :parse => :json 91 | },{}) 92 | subject.send(:build_access_token) 93 | end 94 | end 95 | 96 | describe "#raw_info" do 97 | let(:access_token) { OAuth2::AccessToken.from_hash(client, {}) } 98 | before { subject.stub(:access_token => access_token) } 99 | 100 | context "when scope is snsapi_base" do 101 | let(:access_token) { OAuth2::AccessToken.from_hash(client, { 102 | "openid"=>"openid", 103 | "scope"=>"snsapi_base", 104 | "access_token"=>"access_token" 105 | })} 106 | 107 | specify "only have openid" do 108 | expect(subject.uid).to eq("openid") 109 | expect(subject.raw_info).to eq("openid" => "openid") 110 | end 111 | end 112 | 113 | context "when scope is snsapi_login" do 114 | let(:access_token) { OAuth2::AccessToken.from_hash(client, { 115 | "openid"=>"openid", 116 | "scope"=>"snsapi_login", 117 | "access_token"=>"access_token" 118 | })} 119 | 120 | specify "will query for user info" do 121 | expect(client).to receive(:request) do |verb, path, opts| 122 | expect(verb).to eq(:get) 123 | expect(path).to eq("/sns/userinfo") 124 | expect(opts[:params]).to eq("openid"=> "openid", "lang"=>"zh_CN", "access_token"=> "access_token") 125 | expect(opts[:parse]).to eq(:json) 126 | end.and_return(double("response", parsed: 127 | { 128 | "openid" => "OPENID", 129 | "nickname" => "NICKNAME", 130 | "sex" => "1", 131 | "province" => "PROVINCE", 132 | "city" => "CITY", 133 | "country" => "COUNTRY", 134 | "headimgurl" => "header_image_url", 135 | "privilege" => ["PRIVILEGE1", "PRIVILEGE2"], 136 | "unionid" => "UNIONID" 137 | } 138 | )) 139 | 140 | expect(subject.raw_info).to eq( 141 | { 142 | "openid" => "OPENID", 143 | "nickname" => "NICKNAME", 144 | "sex" => "1", 145 | "province" => "PROVINCE", 146 | "city" => "CITY", 147 | "country" => "COUNTRY", 148 | "headimgurl" => "header_image_url", 149 | "privilege" => ["PRIVILEGE1", "PRIVILEGE2"], 150 | "unionid" => "UNIONID" 151 | } 152 | ) 153 | expect(subject.info).to eq( 154 | { 155 | nickname: "NICKNAME", 156 | sex: "1", 157 | province: "PROVINCE", 158 | city: "CITY", 159 | country: "COUNTRY", 160 | headimgurl: "header_image_url", 161 | image: "header_image_url", 162 | unionid: "UNIONID" 163 | } 164 | ) 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth-wechat-oauth2' 2 | require 'rspec' 3 | require 'byebug' 4 | --------------------------------------------------------------------------------