omniauth-wave-oauth2 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/lib/omniauth/strategies/wave_oauth2.rb +128 -0
- data/lib/omniauth/wave_oauth2/token_client.rb +148 -0
- data/lib/omniauth/wave_oauth2/version.rb +7 -0
- data/lib/omniauth-wave-oauth2.rb +6 -0
- metadata +198 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d1d923ae112d2a304e298303b75f9b9d62686fa67f112fa5933a0450b2efd637
|
|
4
|
+
data.tar.gz: 30c55761978e96f322632a9cde4668da910ffa85b4ae2f707b8aad119ca4de87
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ebc7827b9e163a3d93c0f429be55eb3ec4384231c57d7be0a36b84c12813242505703ea2854b378804d5d6d89661e5240906da9cb1160db0221980c27f0d9ff5
|
|
7
|
+
data.tar.gz: ef442fb36327f6d5f160f053d432ced163e4f4c2d4a0a3c0ef0c1a5e1d7f66e35f1e71f33301757686735e9985ac7fee714655ebd8e529c8d16e4a5bc8c2d444
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-03-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release
|
|
7
|
+
- OmniAuth OAuth2 strategy for Wave (by H&R Block)
|
|
8
|
+
- GraphQL-based user and business info fetching
|
|
9
|
+
- TokenClient for refreshing tokens outside the OmniAuth flow
|
|
10
|
+
- Support for `account:read`, `business:read`, `user:read` scopes
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dan1d
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# OmniAuth Wave OAuth2 Strategy
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/omniauth-wave-oauth2)
|
|
4
|
+
|
|
5
|
+
An OmniAuth strategy for authenticating with [Wave](https://www.waveapps.com/) (by H&R Block) using OAuth 2.0.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'omniauth-wave-oauth2'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Wave Developer Setup
|
|
22
|
+
|
|
23
|
+
1. Sign up at [Wave](https://www.waveapps.com/) and create a business
|
|
24
|
+
2. Go to the [Wave Developer Portal](https://developer.waveapps.com/hc/en-us/articles/360019762711-Manage-Applications)
|
|
25
|
+
3. Create a new application
|
|
26
|
+
4. Note your **Client ID** and **Client Secret**
|
|
27
|
+
5. Add your **Redirect URI** (e.g., `https://yourapp.com/auth/wave_oauth2/callback`)
|
|
28
|
+
|
|
29
|
+
**Note:** Wave requires a [Pro Plan](https://www.waveapps.com/pricing) ($19/mo) for third-party API access.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Standalone OmniAuth
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
37
|
+
provider :wave_oauth2, ENV['WAVE_CLIENT_ID'], ENV['WAVE_CLIENT_SECRET']
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### With Devise
|
|
42
|
+
|
|
43
|
+
In `config/initializers/devise.rb`:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
config.omniauth :wave_oauth2, ENV['WAVE_CLIENT_ID'], ENV['WAVE_CLIENT_SECRET']
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Add to your routes:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Create the callbacks controller:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
59
|
+
def wave_oauth2
|
|
60
|
+
@user = User.from_omniauth(request.env['omniauth.auth'])
|
|
61
|
+
|
|
62
|
+
if @user.persisted?
|
|
63
|
+
sign_in_and_redirect @user, event: :authentication
|
|
64
|
+
else
|
|
65
|
+
redirect_to new_user_registration_url
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Auth Hash
|
|
72
|
+
|
|
73
|
+
Here's an example of the authentication hash available in `request.env['omniauth.auth']`:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
{
|
|
77
|
+
"provider" => "wave_oauth2",
|
|
78
|
+
"uid" => "QnVzaW5lc3M6abc123def456",
|
|
79
|
+
"info" => {
|
|
80
|
+
"email" => "owner@example.com",
|
|
81
|
+
"name" => "Jane Doe",
|
|
82
|
+
"first_name" => "Jane",
|
|
83
|
+
"last_name" => "Doe",
|
|
84
|
+
"business_name" => "Jane's Bakery"
|
|
85
|
+
},
|
|
86
|
+
"credentials" => {
|
|
87
|
+
"token" => "ACCESS_TOKEN",
|
|
88
|
+
"refresh_token" => "REFRESH_TOKEN",
|
|
89
|
+
"expires_at" => 1704067200,
|
|
90
|
+
"expires" => true
|
|
91
|
+
},
|
|
92
|
+
"extra" => {
|
|
93
|
+
"raw_info" => {
|
|
94
|
+
"user_id" => "user-abc",
|
|
95
|
+
"email" => "owner@example.com",
|
|
96
|
+
"first_name" => "Jane",
|
|
97
|
+
"last_name" => "Doe",
|
|
98
|
+
"name" => "Jane Doe",
|
|
99
|
+
"business_id" => "QnVzaW5lc3M6abc123def456",
|
|
100
|
+
"business_name" => "Jane's Bakery"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Wave OAuth2 Specifics
|
|
107
|
+
|
|
108
|
+
- **GraphQL API**: Wave uses a GraphQL API at `gql.waveapps.com/graphql/public` (not REST). User and business info are fetched via GraphQL after token exchange.
|
|
109
|
+
- **Auth Scheme**: Wave requires credentials in the POST body (`auth_scheme: :request_body`), not HTTP Basic Auth.
|
|
110
|
+
- **Token Expiry**: Access tokens expire after ~2 hours. Refresh tokens are long-lived.
|
|
111
|
+
- **UID**: The `uid` is the Wave Business ID (Base64-encoded format like `QnVzaW5lc3M6...`). Falls back to User ID if no business exists.
|
|
112
|
+
- **Scopes**: Requests `account:read business:read user:read` by default.
|
|
113
|
+
|
|
114
|
+
## Token Refresh
|
|
115
|
+
|
|
116
|
+
Wave access tokens expire after ~2 hours. This gem includes a `TokenClient` for refreshing tokens:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
client = OmniAuth::WaveOauth2::TokenClient.new(
|
|
120
|
+
client_id: ENV['WAVE_CLIENT_ID'],
|
|
121
|
+
client_secret: ENV['WAVE_CLIENT_SECRET']
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
result = client.refresh_token(account.refresh_token)
|
|
125
|
+
|
|
126
|
+
if result.success?
|
|
127
|
+
account.update!(
|
|
128
|
+
access_token: result.access_token,
|
|
129
|
+
refresh_token: result.refresh_token,
|
|
130
|
+
token_expires_at: Time.at(result.expires_at)
|
|
131
|
+
)
|
|
132
|
+
else
|
|
133
|
+
Rails.logger.error "Token refresh failed: #{result.error}"
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Check Token Expiration
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# Check if token is expired (with 5-minute buffer by default)
|
|
141
|
+
client.token_expired?(account.token_expires_at)
|
|
142
|
+
|
|
143
|
+
# Custom buffer (e.g., refresh 10 minutes before expiry)
|
|
144
|
+
client.token_expired?(account.token_expires_at, buffer_seconds: 600)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### TokenResult Object
|
|
148
|
+
|
|
149
|
+
| Method | Description |
|
|
150
|
+
|--------|-------------|
|
|
151
|
+
| `success?` | Returns `true` if refresh succeeded |
|
|
152
|
+
| `failure?` | Returns `true` if refresh failed |
|
|
153
|
+
| `access_token` | The new access token |
|
|
154
|
+
| `refresh_token` | The new refresh token |
|
|
155
|
+
| `expires_at` | Unix timestamp when token expires |
|
|
156
|
+
| `expires_in` | Seconds until token expires |
|
|
157
|
+
| `error` | Error message if failed |
|
|
158
|
+
| `raw_response` | Full response hash from Wave |
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
bundle install
|
|
164
|
+
bundle exec rspec
|
|
165
|
+
bundle exec rubocop
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dan1d/omniauth-wave-oauth2.
|
|
171
|
+
|
|
172
|
+
1. Fork it
|
|
173
|
+
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
|
174
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
175
|
+
4. Push to the branch (`git push origin feature/my-new-feature`)
|
|
176
|
+
5. Create a new Pull Request
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'omniauth-oauth2'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module OmniAuth
|
|
8
|
+
module Strategies
|
|
9
|
+
# OmniAuth strategy for Wave (by H&R Block) OAuth2.
|
|
10
|
+
#
|
|
11
|
+
# Wave uses standard OAuth2 with ~2-hour access tokens and refresh tokens.
|
|
12
|
+
# The authorize and token endpoints are on api.waveapps.com.
|
|
13
|
+
# API data is fetched via GraphQL at gql.waveapps.com/graphql/public.
|
|
14
|
+
#
|
|
15
|
+
# Note: Requires Wave Pro Plan ($19/mo) for third-party API access.
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage
|
|
18
|
+
# provider :wave_oauth2, ENV['WAVE_CLIENT_ID'], ENV['WAVE_CLIENT_SECRET']
|
|
19
|
+
#
|
|
20
|
+
# @example With Devise
|
|
21
|
+
# config.omniauth :wave_oauth2, ENV['WAVE_CLIENT_ID'], ENV['WAVE_CLIENT_SECRET']
|
|
22
|
+
#
|
|
23
|
+
class WaveOauth2 < OmniAuth::Strategies::OAuth2
|
|
24
|
+
option :name, 'wave_oauth2'
|
|
25
|
+
|
|
26
|
+
option :client_options, {
|
|
27
|
+
site: 'https://api.waveapps.com',
|
|
28
|
+
authorize_url: '/oauth2/authorize/',
|
|
29
|
+
token_url: '/oauth2/token/',
|
|
30
|
+
auth_scheme: :request_body
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
option :authorize_params, {
|
|
34
|
+
scope: 'account:read business:read user:read'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# UID is the Wave business ID (primary identifier for the connected account)
|
|
38
|
+
uid { raw_info['business_id'] || raw_info['user_id'] }
|
|
39
|
+
|
|
40
|
+
info do
|
|
41
|
+
{
|
|
42
|
+
email: raw_info['email'],
|
|
43
|
+
name: raw_info['name'],
|
|
44
|
+
first_name: raw_info['first_name'],
|
|
45
|
+
last_name: raw_info['last_name'],
|
|
46
|
+
business_name: raw_info['business_name']
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
extra do
|
|
51
|
+
{ raw_info: raw_info }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def raw_info
|
|
55
|
+
@raw_info ||= fetch_user_and_business_info
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Override to strip query params from callback_url for redirect_uri matching.
|
|
59
|
+
# Wave requires the redirect_uri to match exactly.
|
|
60
|
+
def build_access_token
|
|
61
|
+
redirect_uri = callback_url.sub(/\?.*/, '')
|
|
62
|
+
log(:info, "Token exchange — site: #{client.site}, redirect_uri: #{redirect_uri}")
|
|
63
|
+
verifier = request.params['code']
|
|
64
|
+
client.auth_code.get_token(
|
|
65
|
+
verifier,
|
|
66
|
+
{ redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true)),
|
|
67
|
+
deep_symbolize(options.auth_token_params)
|
|
68
|
+
)
|
|
69
|
+
rescue ::OAuth2::Error => e
|
|
70
|
+
log(:error, "Token exchange FAILED: status=#{e.response&.status} body=#{e.response&.body}")
|
|
71
|
+
raise
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
GRAPHQL_URL = 'https://gql.waveapps.com/graphql/public'
|
|
77
|
+
|
|
78
|
+
USER_QUERY = <<~GRAPHQL
|
|
79
|
+
query {
|
|
80
|
+
user {
|
|
81
|
+
id
|
|
82
|
+
firstName
|
|
83
|
+
lastName
|
|
84
|
+
defaultEmail
|
|
85
|
+
}
|
|
86
|
+
businesses(page: 1, pageSize: 1) {
|
|
87
|
+
edges {
|
|
88
|
+
node {
|
|
89
|
+
id
|
|
90
|
+
name
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
GRAPHQL
|
|
96
|
+
|
|
97
|
+
def fetch_user_and_business_info
|
|
98
|
+
response = access_token.post(GRAPHQL_URL, {
|
|
99
|
+
body: { query: USER_QUERY }.to_json,
|
|
100
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
data = JSON.parse(response.body)['data'] || {}
|
|
104
|
+
user = data['user'] || {}
|
|
105
|
+
business = data.dig('businesses', 'edges', 0, 'node') || {}
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
'user_id' => user['id'],
|
|
109
|
+
'email' => user['defaultEmail'],
|
|
110
|
+
'first_name' => user['firstName'],
|
|
111
|
+
'last_name' => user['lastName'],
|
|
112
|
+
'name' => [user['firstName'], user['lastName']].compact.join(' '),
|
|
113
|
+
'business_id' => business['id'],
|
|
114
|
+
'business_name' => business['name']
|
|
115
|
+
}
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
log(:warn, "Failed to fetch user/business info: #{e.message}")
|
|
118
|
+
{ 'user_id' => nil }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def log(level, message)
|
|
122
|
+
return unless defined?(OmniAuth.logger) && OmniAuth.logger
|
|
123
|
+
|
|
124
|
+
OmniAuth.logger.send(level, "[WaveOauth2] #{message}")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module OmniAuth
|
|
7
|
+
module WaveOauth2
|
|
8
|
+
# Client for managing Wave OAuth2 tokens outside the OmniAuth flow.
|
|
9
|
+
#
|
|
10
|
+
# Wave access tokens expire after ~2 hours. Use this client to refresh
|
|
11
|
+
# tokens in background jobs or API services.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# client = OmniAuth::WaveOauth2::TokenClient.new(
|
|
15
|
+
# client_id: ENV['WAVE_CLIENT_ID'],
|
|
16
|
+
# client_secret: ENV['WAVE_CLIENT_SECRET']
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# result = client.refresh_token(account.refresh_token)
|
|
20
|
+
# if result.success?
|
|
21
|
+
# account.update!(
|
|
22
|
+
# access_token: result.access_token,
|
|
23
|
+
# refresh_token: result.refresh_token,
|
|
24
|
+
# token_expires_at: Time.at(result.expires_at)
|
|
25
|
+
# )
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
class TokenClient
|
|
29
|
+
# Result object for token operations
|
|
30
|
+
class TokenResult
|
|
31
|
+
attr_reader :access_token, :refresh_token, :expires_at, :expires_in, :error, :raw_response
|
|
32
|
+
|
|
33
|
+
def initialize(success:, access_token: nil, refresh_token: nil, expires_at: nil, expires_in: nil,
|
|
34
|
+
error: nil, raw_response: nil)
|
|
35
|
+
@success = success
|
|
36
|
+
@access_token = access_token
|
|
37
|
+
@refresh_token = refresh_token
|
|
38
|
+
@expires_at = expires_at
|
|
39
|
+
@expires_in = expires_in
|
|
40
|
+
@error = error
|
|
41
|
+
@raw_response = raw_response
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def success?
|
|
45
|
+
@success
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def failure?
|
|
49
|
+
!@success
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
TOKEN_URL = 'https://api.waveapps.com/oauth2/token/'
|
|
54
|
+
|
|
55
|
+
attr_reader :client_id, :client_secret
|
|
56
|
+
|
|
57
|
+
# Initialize a new TokenClient
|
|
58
|
+
#
|
|
59
|
+
# @param client_id [String] Your Wave App Client ID
|
|
60
|
+
# @param client_secret [String] Your Wave App Client Secret
|
|
61
|
+
def initialize(client_id:, client_secret:)
|
|
62
|
+
@client_id = client_id
|
|
63
|
+
@client_secret = client_secret
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Refresh an access token using a refresh token
|
|
67
|
+
#
|
|
68
|
+
# @param refresh_token [String] The refresh token to use
|
|
69
|
+
# @return [TokenResult] Result object with new tokens or error
|
|
70
|
+
def refresh_token(refresh_token)
|
|
71
|
+
return TokenResult.new(success: false, error: 'Refresh token is required') if refresh_token.nil? || refresh_token.empty?
|
|
72
|
+
|
|
73
|
+
response = make_refresh_request(refresh_token)
|
|
74
|
+
|
|
75
|
+
if response.success?
|
|
76
|
+
parse_success_response(response)
|
|
77
|
+
else
|
|
78
|
+
parse_error_response(response)
|
|
79
|
+
end
|
|
80
|
+
rescue Faraday::Error => e
|
|
81
|
+
TokenResult.new(success: false, error: "Network error: #{e.message}")
|
|
82
|
+
rescue JSON::ParserError => e
|
|
83
|
+
TokenResult.new(success: false, error: "Invalid JSON response: #{e.message}")
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
TokenResult.new(success: false, error: "Unexpected error: #{e.message}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if a token is expired or about to expire
|
|
89
|
+
#
|
|
90
|
+
# @param expires_at [Time, Integer] Token expiration time
|
|
91
|
+
# @param buffer_seconds [Integer] Buffer before expiration (default: 300 = 5 minutes)
|
|
92
|
+
# @return [Boolean] True if token is expired or will expire within buffer
|
|
93
|
+
def token_expired?(expires_at, buffer_seconds: 300)
|
|
94
|
+
return true if expires_at.nil?
|
|
95
|
+
|
|
96
|
+
expires_at_time = expires_at.is_a?(Integer) ? Time.at(expires_at) : expires_at
|
|
97
|
+
Time.now >= (expires_at_time - buffer_seconds)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def make_refresh_request(refresh_token)
|
|
103
|
+
Faraday.post(TOKEN_URL) do |req|
|
|
104
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
105
|
+
req.headers['Accept'] = 'application/json'
|
|
106
|
+
req.body = URI.encode_www_form(
|
|
107
|
+
grant_type: 'refresh_token',
|
|
108
|
+
client_id: client_id,
|
|
109
|
+
client_secret: client_secret,
|
|
110
|
+
refresh_token: refresh_token
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_success_response(response)
|
|
116
|
+
data = JSON.parse(response.body)
|
|
117
|
+
|
|
118
|
+
expires_in = data['expires_in']&.to_i
|
|
119
|
+
expires_at = expires_in ? Time.now.to_i + expires_in : nil
|
|
120
|
+
|
|
121
|
+
TokenResult.new(
|
|
122
|
+
success: true,
|
|
123
|
+
access_token: data['access_token'],
|
|
124
|
+
refresh_token: data['refresh_token'],
|
|
125
|
+
expires_in: expires_in,
|
|
126
|
+
expires_at: expires_at,
|
|
127
|
+
raw_response: data
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_error_response(response)
|
|
132
|
+
error_data = begin
|
|
133
|
+
JSON.parse(response.body)
|
|
134
|
+
rescue JSON::ParserError
|
|
135
|
+
{ 'message' => response.body }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
error_message = error_data['error_description'] || error_data['error'] || "HTTP #{response.status}"
|
|
139
|
+
|
|
140
|
+
TokenResult.new(
|
|
141
|
+
success: false,
|
|
142
|
+
error: error_message,
|
|
143
|
+
raw_response: error_data
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omniauth-wave-oauth2
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- dan1d
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: omniauth-oauth2
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.8'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.8'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: bundler
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rack-test
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '2.1'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '2.1'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rake
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '13.0'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '13.0'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: rspec
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '3.12'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '3.12'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: rubocop
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '1.75'
|
|
109
|
+
type: :development
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '1.75'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: rubocop-rspec
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '3.5'
|
|
123
|
+
type: :development
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '3.5'
|
|
130
|
+
- !ruby/object:Gem::Dependency
|
|
131
|
+
name: simplecov
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0.22'
|
|
137
|
+
type: :development
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - "~>"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0.22'
|
|
144
|
+
- !ruby/object:Gem::Dependency
|
|
145
|
+
name: webmock
|
|
146
|
+
requirement: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - "~>"
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '3.18'
|
|
151
|
+
type: :development
|
|
152
|
+
prerelease: false
|
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - "~>"
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '3.18'
|
|
158
|
+
description: An OmniAuth strategy for authenticating with Wave accounting (by H&R
|
|
159
|
+
Block) using OAuth 2.0. Fetches user and business info via Wave's GraphQL API.
|
|
160
|
+
email:
|
|
161
|
+
- dan1d@users.noreply.github.com
|
|
162
|
+
executables: []
|
|
163
|
+
extensions: []
|
|
164
|
+
extra_rdoc_files: []
|
|
165
|
+
files:
|
|
166
|
+
- CHANGELOG.md
|
|
167
|
+
- LICENSE.txt
|
|
168
|
+
- README.md
|
|
169
|
+
- lib/omniauth-wave-oauth2.rb
|
|
170
|
+
- lib/omniauth/strategies/wave_oauth2.rb
|
|
171
|
+
- lib/omniauth/wave_oauth2/token_client.rb
|
|
172
|
+
- lib/omniauth/wave_oauth2/version.rb
|
|
173
|
+
homepage: https://github.com/dan1d/omniauth-wave-oauth2
|
|
174
|
+
licenses:
|
|
175
|
+
- MIT
|
|
176
|
+
metadata:
|
|
177
|
+
homepage_uri: https://github.com/dan1d/omniauth-wave-oauth2
|
|
178
|
+
source_code_uri: https://github.com/dan1d/omniauth-wave-oauth2
|
|
179
|
+
changelog_uri: https://github.com/dan1d/omniauth-wave-oauth2/blob/main/CHANGELOG.md
|
|
180
|
+
rubygems_mfa_required: 'true'
|
|
181
|
+
rdoc_options: []
|
|
182
|
+
require_paths:
|
|
183
|
+
- lib
|
|
184
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
185
|
+
requirements:
|
|
186
|
+
- - ">="
|
|
187
|
+
- !ruby/object:Gem::Version
|
|
188
|
+
version: 3.0.0
|
|
189
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
190
|
+
requirements:
|
|
191
|
+
- - ">="
|
|
192
|
+
- !ruby/object:Gem::Version
|
|
193
|
+
version: '0'
|
|
194
|
+
requirements: []
|
|
195
|
+
rubygems_version: 3.6.9
|
|
196
|
+
specification_version: 4
|
|
197
|
+
summary: OmniAuth OAuth2 strategy for Wave (by H&R Block)
|
|
198
|
+
test_files: []
|