Class: OmniAuth::Strategies::Shopline

Inherits:
OAuth2
  • Object
show all
Defined in:
lib/omniauth/strategies/shopline.rb

Overview

OmniAuth strategy for SHOPLINE's OAuth 2.0 app authorization flow.

https://developer.shopline.com/docs/apps/api-instructions-for-use/app-authorization/

SHOPLINE deviates from plain OAuth 2 in three ways that this strategy has to handle itself rather than inherit:

  1. The authorize endpoint is a hash-routed admin page, so its query string sits after the # fragment and cannot be built by OAuth2::Client#authorize_url.
  2. There is no state parameter. customField is the documented pass-through, so the CSRF nonce travels in it (see #verify_state).
  3. The token endpoint authenticates with appkey/timestamp/sign headers instead of client credentials, and signs POSTs over the request body.

Constant Summary collapse

SITE_TEMPLATE =
"https://%<handle>s.myshopline.com"
DEFAULT_SITE =

The placeholder is replaced with the configured handle unless the consumer supplies a site of their own.

"https://{handle}.myshopline.com"
STATE_SESSION_KEY =
"omniauth.shopline.state"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, *args, &block) ⇒ Shopline

Returns a new instance of Shopline.



59
60
61
62
63
64
65
66
67
68
# File 'lib/omniauth/strategies/shopline.rb', line 59

def initialize(app, *args, &block)
  super

  @handle = options[:handle] || raise(ArgumentError, "handle is required")

  site = options.client_options[:site]
  return unless site.nil? || site.to_s.include?("{handle}")

  options.client_options[:site] = format(SITE_TEMPLATE, handle: @handle)
end

Instance Attribute Details

#handleObject (readonly)

Returns the value of attribute handle.



57
58
59
# File 'lib/omniauth/strategies/shopline.rb', line 57

def handle
  @handle
end

Instance Method Details

#app_keyObject



70
71
72
# File 'lib/omniauth/strategies/shopline.rb', line 70

def app_key
  options.app_key || options.client_id
end

#app_secretObject



74
75
76
# File 'lib/omniauth/strategies/shopline.rb', line 74

def app_secret
  options.app_secret || options.client_secret
end

#authorize_urlObject

The authorize URL is built by hand: /admin/oauth-web/#/oauth/authorize is a client-side route, so the query string belongs after the fragment. Handing the path to OAuth2::Client#authorize_url would produce /admin/oauth-web/?appKey=...#/oauth/authorize, which the page never reads.



82
83
84
85
86
87
# File 'lib/omniauth/strategies/shopline.rb', line 82

def authorize_url
  client_options = options.client_options
  query = URI.encode_www_form(authorize_params_for_shopline)

  "#{client_options[:site]}#{client_options[:authorize_url]}?#{query}"
end

#build_access_tokenObject

POST /admin/oauth/token/create, authenticated by the appkey/timestamp/sign headers rather than by client credentials, and answered with the token nested under data.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/omniauth/strategies/shopline.rb', line 111

def build_access_token
  timestamp = current_timestamp
  body = JSON.generate(code: request.params["code"])

  response = client.request(
    :post,
    options.client_options[:token_url],
    body: body,
    headers: {
      "Content-Type" => "application/json",
      "appkey" => app_key.to_s,
      "timestamp" => timestamp.to_s,
      "sign" => signature_for_body(body, timestamp)
    }
  )

  access_token_from(response)
end

#callback_phaseObject



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/omniauth/strategies/shopline.rb', line 93

def callback_phase
  error = request.params["error"].to_s
  unless error.empty?
    return fail!(error.to_sym, CallbackError.new(error, request.params["error_description"]))
  end

  return fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected")) unless valid_state?

  unless valid_signature?
    return fail!(:invalid_signature, CallbackError.new(:invalid_signature, "Signature verification failed"))
  end

  super
end

#callback_urlObject

OmniAuth::Strategy#callback_url appends the callback request's own query string, which would send those params to SHOPLINE as part of redirect_uri and fail the registered-URL match. Note that omniauth 2's callback_path already carries SCRIPT_NAME, so mount prefixes must not be added again.



134
135
136
# File 'lib/omniauth/strategies/shopline.rb', line 134

def callback_url
  options[:callback_url] || options[:redirect_uri] || (full_host + callback_path)
end

#request_phaseObject



89
90
91
# File 'lib/omniauth/strategies/shopline.rb', line 89

def request_phase
  redirect authorize_url
end