omniauth-v2-ynab 0.0.1
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/.github/workflows/ci.yml +36 -0
- data/.github/workflows/release.yml +116 -0
- data/.rspec +2 -0
- data/.rubocop.yml +79 -0
- data/CLAUDE.md +61 -0
- data/Gemfile +10 -0
- data/LICENSE.md +21 -0
- data/README.md +178 -0
- data/Rakefile +18 -0
- data/lib/omniauth/strategies/ynab.rb +175 -0
- data/lib/omniauth-ynab/version.rb +5 -0
- data/lib/omniauth-ynab.rb +2 -0
- data/omniauth-v2-ynab.gemspec +24 -0
- data/spec/helper.rb +17 -0
- data/spec/omniauth/strategies/ynab_spec.rb +196 -0
- metadata +87 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e4f8a1758c2f8734a8a66884697297b4606b55db826322c6ccbd84f7d8525787
|
|
4
|
+
data.tar.gz: d503e5f74a50911cc1df6d32304b7d3bc523924dfbcbc7165a4f335e3d2b3423
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0f37fc7552b87440d60503431fc5849975bc8f914c5aeb02717fc6e57d6fd7533e8baac82224e3be09a75f5299a951d51c02c408ad5e958a4b06a1cc2b57c1ca
|
|
7
|
+
data.tar.gz: d9a6f8c551016516a8bc825b1f095ebddefee1a485e595943e55cdf48c8e66c643c63c28fca881129593682249288baf3a14008f5a7bd4e564abdae345f84dfc
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Ruby ${{ matrix.ruby }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
ruby:
|
|
20
|
+
- "3.1"
|
|
21
|
+
- "3.2"
|
|
22
|
+
- "3.3"
|
|
23
|
+
- "3.4"
|
|
24
|
+
- head
|
|
25
|
+
continue-on-error: ${{ matrix.ruby == 'head' }}
|
|
26
|
+
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
|
|
30
|
+
- uses: ruby/setup-ruby@v1
|
|
31
|
+
with:
|
|
32
|
+
ruby-version: ${{ matrix.ruby }}
|
|
33
|
+
bundler-cache: true
|
|
34
|
+
|
|
35
|
+
- name: Run tests and lint
|
|
36
|
+
run: bundle exec rake
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: ["CI"]
|
|
6
|
+
branches: [main]
|
|
7
|
+
types: [completed]
|
|
8
|
+
|
|
9
|
+
# Never cancel a release mid-flight.
|
|
10
|
+
concurrency:
|
|
11
|
+
group: release
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release:
|
|
20
|
+
# Only run when CI passed, and not on auto-bump commits (loop prevention).
|
|
21
|
+
if: |
|
|
22
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
23
|
+
!startsWith(github.event.workflow_run.head_commit.message, 'Bump version to')
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: rubygems
|
|
26
|
+
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
ref: ${{ github.event.workflow_run.head_sha }}
|
|
32
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
+
|
|
34
|
+
- uses: ruby/setup-ruby@v1
|
|
35
|
+
with:
|
|
36
|
+
ruby-version: "3.3"
|
|
37
|
+
bundler-cache: true
|
|
38
|
+
|
|
39
|
+
- name: Configure git
|
|
40
|
+
run: |
|
|
41
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
42
|
+
git config user.name "github-actions[bot]"
|
|
43
|
+
|
|
44
|
+
- name: Resolve version
|
|
45
|
+
id: version
|
|
46
|
+
run: |
|
|
47
|
+
CURRENT=$(ruby -e "require_relative 'lib/omniauth-ynab/version'; puts OmniAuth::YNAB::VERSION")
|
|
48
|
+
LATEST_TAG=$(git tag -l "v*" --sort=-version:refname | head -1)
|
|
49
|
+
LATEST=${LATEST_TAG#v}
|
|
50
|
+
[ -z "$LATEST" ] && LATEST="0.0.0"
|
|
51
|
+
|
|
52
|
+
if git rev-parse "v$CURRENT" >/dev/null 2>&1; then
|
|
53
|
+
echo "Tag v$CURRENT already exists — nothing to release."
|
|
54
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
55
|
+
elif gh release view "v$CURRENT" --repo ${{ github.repository }} >/dev/null 2>&1; then
|
|
56
|
+
echo "GitHub release v$CURRENT already exists — nothing to release."
|
|
57
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
58
|
+
elif curl -sf "https://rubygems.org/api/v1/versions/omniauth-v2-ynab.json" | grep -q "\"number\":\"$CURRENT\""; then
|
|
59
|
+
echo "v$CURRENT already published on RubyGems — nothing to release."
|
|
60
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
61
|
+
elif [ "$CURRENT" = "$LATEST" ]; then
|
|
62
|
+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
|
63
|
+
NEW="$MAJOR.$((MINOR + 1)).0"
|
|
64
|
+
sed -i "s/VERSION = \"$CURRENT\".freeze/VERSION = \"$NEW\".freeze/" lib/omniauth-ynab/version.rb
|
|
65
|
+
echo "version=$NEW" >> "$GITHUB_OUTPUT"
|
|
66
|
+
echo "bumped=true" >> "$GITHUB_OUTPUT"
|
|
67
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
68
|
+
else
|
|
69
|
+
echo "version=$CURRENT" >> "$GITHUB_OUTPUT"
|
|
70
|
+
echo "bumped=false" >> "$GITHUB_OUTPUT"
|
|
71
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
- name: Build gem
|
|
75
|
+
if: steps.version.outputs.skip != 'true'
|
|
76
|
+
run: gem build omniauth-v2-ynab.gemspec
|
|
77
|
+
|
|
78
|
+
- name: Debug OIDC claims
|
|
79
|
+
if: steps.version.outputs.skip != 'true'
|
|
80
|
+
run: |
|
|
81
|
+
TOKEN=$(curl -sf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
|
|
82
|
+
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=rubygems.org" | jq -r '.value')
|
|
83
|
+
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
|
|
84
|
+
|
|
85
|
+
- name: Configure RubyGems credentials (OIDC)
|
|
86
|
+
if: steps.version.outputs.skip != 'true'
|
|
87
|
+
uses: rubygems/configure-rubygems-credentials@v1.0.0
|
|
88
|
+
with:
|
|
89
|
+
role-to-assume: rg_oidc_akr_aq2q9ba42vwb9agghxx7
|
|
90
|
+
|
|
91
|
+
- name: Push to RubyGems
|
|
92
|
+
if: steps.version.outputs.skip != 'true'
|
|
93
|
+
run: gem push omniauth-v2-ynab-*.gem
|
|
94
|
+
|
|
95
|
+
- name: Tag release
|
|
96
|
+
if: steps.version.outputs.skip != 'true'
|
|
97
|
+
run: |
|
|
98
|
+
git tag "v${{ steps.version.outputs.version }}"
|
|
99
|
+
git push origin "v${{ steps.version.outputs.version }}"
|
|
100
|
+
|
|
101
|
+
- name: Create GitHub Release
|
|
102
|
+
if: steps.version.outputs.skip != 'true'
|
|
103
|
+
env:
|
|
104
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
105
|
+
run: |
|
|
106
|
+
gh release create "v${{ steps.version.outputs.version }}" \
|
|
107
|
+
omniauth-v2-ynab-*.gem \
|
|
108
|
+
--title "v${{ steps.version.outputs.version }}" \
|
|
109
|
+
--generate-notes
|
|
110
|
+
|
|
111
|
+
- name: Commit version bump
|
|
112
|
+
if: steps.version.outputs.bumped == 'true'
|
|
113
|
+
run: |
|
|
114
|
+
git add lib/omniauth-ynab/version.rb
|
|
115
|
+
git commit -m "Bump version to ${{ steps.version.outputs.version }}"
|
|
116
|
+
git push origin HEAD:main
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 3.1
|
|
4
|
+
|
|
5
|
+
Layout/AccessModifierIndentation:
|
|
6
|
+
EnforcedStyle: outdent
|
|
7
|
+
|
|
8
|
+
Layout/LineLength:
|
|
9
|
+
AllowURI: true
|
|
10
|
+
Enabled: false
|
|
11
|
+
|
|
12
|
+
Layout/SpaceInsideHashLiteralBraces:
|
|
13
|
+
EnforcedStyle: no_space
|
|
14
|
+
|
|
15
|
+
Lint/MissingSuper:
|
|
16
|
+
Enabled: false
|
|
17
|
+
|
|
18
|
+
Lint/ScriptPermission:
|
|
19
|
+
Enabled: false
|
|
20
|
+
|
|
21
|
+
Metrics/AbcSize:
|
|
22
|
+
Enabled: false
|
|
23
|
+
|
|
24
|
+
Metrics/BlockLength:
|
|
25
|
+
Enabled: false
|
|
26
|
+
|
|
27
|
+
Metrics/BlockNesting:
|
|
28
|
+
Max: 2
|
|
29
|
+
|
|
30
|
+
Metrics/ClassLength:
|
|
31
|
+
Max: 150
|
|
32
|
+
|
|
33
|
+
Metrics/CyclomaticComplexity:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
Metrics/MethodLength:
|
|
37
|
+
CountComments: false
|
|
38
|
+
Max: 20
|
|
39
|
+
|
|
40
|
+
Metrics/ParameterLists:
|
|
41
|
+
Max: 4
|
|
42
|
+
CountKeywordArgs: true
|
|
43
|
+
|
|
44
|
+
Metrics/PerceivedComplexity:
|
|
45
|
+
Enabled: false
|
|
46
|
+
|
|
47
|
+
Naming/FileName:
|
|
48
|
+
Enabled: false
|
|
49
|
+
|
|
50
|
+
Naming/PredicateMethod:
|
|
51
|
+
Enabled: false
|
|
52
|
+
|
|
53
|
+
Style/Documentation:
|
|
54
|
+
Enabled: false
|
|
55
|
+
|
|
56
|
+
Style/DoubleNegation:
|
|
57
|
+
Enabled: false
|
|
58
|
+
|
|
59
|
+
Style/FrozenStringLiteralComment:
|
|
60
|
+
Enabled: false
|
|
61
|
+
|
|
62
|
+
Style/HashSyntax:
|
|
63
|
+
EnforcedStyle: hash_rockets
|
|
64
|
+
EnforcedShorthandSyntax: never
|
|
65
|
+
|
|
66
|
+
Style/StderrPuts:
|
|
67
|
+
Enabled: false
|
|
68
|
+
|
|
69
|
+
Style/StringLiterals:
|
|
70
|
+
EnforcedStyle: double_quotes
|
|
71
|
+
|
|
72
|
+
Style/TrailingCommaInArguments:
|
|
73
|
+
EnforcedStyleForMultiline: comma
|
|
74
|
+
|
|
75
|
+
Style/TrailingCommaInArrayLiteral:
|
|
76
|
+
EnforcedStyleForMultiline: comma
|
|
77
|
+
|
|
78
|
+
Style/TrailingCommaInHashLiteral:
|
|
79
|
+
EnforcedStyleForMultiline: comma
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# omniauth-v2-ynab — Claude guidance
|
|
2
|
+
|
|
3
|
+
## What this repo is
|
|
4
|
+
|
|
5
|
+
A single-strategy OmniAuth gem for YNAB OAuth2. It is a **direct implementation** of the OmniAuth strategy interface — it does not inherit from `omniauth-oauth2`. Everything lives in one strategy file.
|
|
6
|
+
|
|
7
|
+
## Key files
|
|
8
|
+
|
|
9
|
+
| File | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `lib/omniauth/strategies/ynab.rb` | The strategy — start here for any auth-flow changes. |
|
|
12
|
+
| `lib/omniauth-ynab/version.rb` | Gem version constant. |
|
|
13
|
+
| `omniauth-v2-ynab.gemspec` | Dependencies. Runtime: `omniauth ~> 2.0`, `oauth2 ~> 2.0`. |
|
|
14
|
+
| `spec/omniauth/strategies/ynab_spec.rb` | Full RSpec suite. |
|
|
15
|
+
| `spec/helper.rb` | RSpec config — loads `omniauth-ynab`, sets up Rack::Test and WebMock. |
|
|
16
|
+
| `.rubocop.yml` | RuboCop 1.x config. Target Ruby: 3.1. |
|
|
17
|
+
| `.github/workflows/ci.yml` | CI — runs `bundle exec rake` (spec + rubocop) on Ruby 3.1–3.4 + head. |
|
|
18
|
+
| `.github/workflows/release.yml` | Release — triggers after CI passes on main. Auto-bumps minor version unless already bumped, tags, creates GitHub Release, pushes to RubyGems. |
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bundle exec rspec # tests only
|
|
24
|
+
bundle exec rubocop # lint only
|
|
25
|
+
bundle exec rake # both (matches CI)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Architecture notes
|
|
29
|
+
|
|
30
|
+
- The strategy includes `OmniAuth::Strategy` directly and implements `request_phase`, `callback_phase`, `authorize_params`, `token_params`, and `build_access_token` by hand.
|
|
31
|
+
- CSRF state is stored in the Rack session under `omniauth.state` and validated in `callback_phase`.
|
|
32
|
+
- PKCE is opt-in (`pkce: true`). The verifier is stored in the session under `omniauth.pkce.verifier` between request and callback phase.
|
|
33
|
+
- `deep_symbolize` is a local helper because `oauth2 2.x` requires symbolized keys in `OAuth2::Client` options.
|
|
34
|
+
- `omniauth-rails_csrf_protection` is a **runtime requirement** for Rails apps using omniauth 2.x — it is listed as a dev dependency in the gemspec and should be called out in app-level Gemfiles.
|
|
35
|
+
|
|
36
|
+
## Dependency constraints
|
|
37
|
+
|
|
38
|
+
- `omniauth ~> 2.0` — omniauth 2.x changed default allowed request methods to POST only; CSRF is handled externally by `omniauth-rails_csrf_protection`.
|
|
39
|
+
- `oauth2 ~> 2.0` — `get_token` params are now fully merged in the second positional argument; the third argument is `access_token_opts`.
|
|
40
|
+
- Do not re-add `simplecov`, `coveralls`, or `omniauth-oauth2` — these were in the original but are not used.
|
|
41
|
+
|
|
42
|
+
## Testing conventions
|
|
43
|
+
|
|
44
|
+
- Tests use `OmniAuth.config.test_mode = true` in `before`/`after` blocks — do not remove these.
|
|
45
|
+
- `CallbackError` specs test exact string output from `#message`, not regex — keep assertions strict.
|
|
46
|
+
- New auth-flow behaviour needs both a positive and a failure-path test.
|
|
47
|
+
|
|
48
|
+
## Releasing
|
|
49
|
+
|
|
50
|
+
Releases are fully automated. Push to `main` → CI runs → on success, the release workflow:
|
|
51
|
+
- Compares `VERSION` in `lib/omniauth-ynab/version.rb` against the latest git tag
|
|
52
|
+
- If already bumped (version > tag): tags, builds, publishes to RubyGems, creates GitHub Release
|
|
53
|
+
- If not bumped (version == tag): auto-bumps the minor version, then does the above
|
|
54
|
+
|
|
55
|
+
To release a specific version (e.g. a major bump), just update `version.rb` manually before pushing. The release workflow will detect the higher version and publish it without an additional bump.
|
|
56
|
+
|
|
57
|
+
The `RUBYGEMS_API_KEY` secret must be set in the repository settings for gem pushes to work.
|
|
58
|
+
|
|
59
|
+
## Versioning
|
|
60
|
+
|
|
61
|
+
Follow semver. Published as `omniauth-v2-ynab` on RubyGems to distinguish from the unmaintained `omniauth-ynab` gem. The Ruby constant (`OmniAuth::Strategies::YNAB`) is unchanged.
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (C) 2014 Michael Bleigh, Erik Michaels-Ober and Intridea, Inc.
|
|
2
|
+
Copyright (C) 2024 Mike Berkman
|
|
3
|
+
Copyright (C) 2026 tataihono
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# omniauth-v2-ynab
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/omniauth-v2-ynab)
|
|
4
|
+
[](https://github.com/tataihono/omniauth-v2-ynab/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
OmniAuth strategy for [YNAB (You Need A Budget)](https://www.youneedabudget.com/) OAuth2.
|
|
7
|
+
|
|
8
|
+
Compatible with **omniauth 2.x**, **oauth2 2.x**, and **omniauth-rails_csrf_protection 1.x**.
|
|
9
|
+
|
|
10
|
+
> **Note:** This is a maintained fork of the original [`omniauth-ynab`](https://rubygems.org/gems/omniauth-ynab) gem, updated for the omniauth 2.x / oauth2 2.x ecosystem.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add to your `Gemfile`:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "omniauth-v2-ynab"
|
|
20
|
+
gem "omniauth-rails_csrf_protection" # required for Rails with omniauth 2.x
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then run:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Register a YNAB application
|
|
34
|
+
|
|
35
|
+
Create an OAuth application at [app.youneedabudget.com/oauth/applications](https://app.youneedabudget.com/oauth/applications). Set the redirect URI to match your callback URL (e.g. `https://yourapp.com/auth/ynab/callback`).
|
|
36
|
+
|
|
37
|
+
### Rails
|
|
38
|
+
|
|
39
|
+
In `config/initializers/omniauth.rb`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
43
|
+
provider :ynab, ENV["YNAB_CLIENT_ID"], ENV["YNAB_CLIENT_SECRET"]
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Add routes:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# config/routes.rb
|
|
51
|
+
get "/auth/:provider/callback", to: "sessions#create"
|
|
52
|
+
get "/auth/failure", to: "sessions#failure"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Trigger the flow from a view using a CSRF-protected link (provided by `omniauth-rails_csrf_protection`):
|
|
56
|
+
|
|
57
|
+
```erb
|
|
58
|
+
<%= link_to "Connect YNAB", "/auth/ynab", method: :post %>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Handle the callback in your controller:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class SessionsController < ApplicationController
|
|
65
|
+
def create
|
|
66
|
+
auth = request.env["omniauth.auth"]
|
|
67
|
+
token = auth.credentials.token
|
|
68
|
+
expires_at = auth.credentials.expires_at
|
|
69
|
+
# store token and create/find the user ...
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def failure
|
|
73
|
+
# request.env["omniauth.error"] contains the error
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Rack (non-Rails)
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
use OmniAuth::Builder do
|
|
82
|
+
provider :ynab, ENV["YNAB_CLIENT_ID"], ENV["YNAB_CLIENT_SECRET"]
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Configuration options
|
|
89
|
+
|
|
90
|
+
All options are passed as keyword arguments to `provider`.
|
|
91
|
+
|
|
92
|
+
| Option | Default | Description |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `client_options` | `{site: "https://app.youneedabudget.com"}` | Override any `OAuth2::Client` option, e.g. `authorize_url`. |
|
|
95
|
+
| `authorize_params` | `{}` | Extra params appended to the authorization redirect URL. |
|
|
96
|
+
| `authorize_options` | `[:scope]` | Top-level option keys that should be forwarded as authorize params. |
|
|
97
|
+
| `token_params` | `{}` | Extra params sent in the token exchange request. |
|
|
98
|
+
| `token_options` | `[]` | Top-level option keys forwarded as token params. |
|
|
99
|
+
| `provider_ignores_state` | `false` | Skip CSRF state validation (not recommended). |
|
|
100
|
+
| `pkce` | `false` | Enable PKCE (S256 code challenge). Recommended for public clients. |
|
|
101
|
+
|
|
102
|
+
### PKCE
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
provider :ynab, ENV["YNAB_CLIENT_ID"], ENV["YNAB_CLIENT_SECRET"], pkce: true
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Overriding the YNAB endpoint (e.g. for testing)
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
provider :ynab, "id", "secret",
|
|
112
|
+
client_options: {site: "https://staging.example.com"}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Credentials
|
|
118
|
+
|
|
119
|
+
After a successful callback, `request.env["omniauth.auth"].credentials` contains:
|
|
120
|
+
|
|
121
|
+
| Key | Description |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `token` | The OAuth2 access token. |
|
|
124
|
+
| `refresh_token` | Present if the token is expiring and a refresh token was issued. |
|
|
125
|
+
| `expires_at` | Unix timestamp of expiry (if applicable). |
|
|
126
|
+
| `expires` | Boolean — whether the token expires. |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
### Prerequisites
|
|
133
|
+
|
|
134
|
+
- Ruby 3.1+
|
|
135
|
+
- Bundler 2.x
|
|
136
|
+
|
|
137
|
+
### Setup
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
git clone https://github.com/tataihono/omniauth-v2-ynab.git
|
|
141
|
+
cd omniauth-ynab
|
|
142
|
+
bundle install
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Running tests
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
bundle exec rspec
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Linting
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
bundle exec rubocop
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Run both (same as CI)
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
bundle exec rake
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Contributing
|
|
166
|
+
|
|
167
|
+
1. Fork the repo and create a branch from `main`.
|
|
168
|
+
2. Add tests for any new behaviour.
|
|
169
|
+
3. Ensure `bundle exec rake` passes.
|
|
170
|
+
4. Open a pull request.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT. See [LICENSE.md](LICENSE.md) for details.
|
|
177
|
+
|
|
178
|
+
Original gem by [Mike Berkman](https://github.com/berkman/omniauth-ynab).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
require "bundler/gem_tasks"
|
|
3
|
+
require "rspec/core/rake_task"
|
|
4
|
+
|
|
5
|
+
RSpec::Core::RakeTask.new
|
|
6
|
+
|
|
7
|
+
task :test => :spec
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
require "rubocop/rake_task"
|
|
11
|
+
RuboCop::RakeTask.new
|
|
12
|
+
rescue LoadError
|
|
13
|
+
task :rubocop do
|
|
14
|
+
$stderr.puts "RuboCop is disabled"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task :default => %i[spec rubocop]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
require "oauth2"
|
|
2
|
+
require "omniauth"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "timeout"
|
|
6
|
+
|
|
7
|
+
module OmniAuth
|
|
8
|
+
module Strategies
|
|
9
|
+
class YNAB
|
|
10
|
+
include OmniAuth::Strategy
|
|
11
|
+
|
|
12
|
+
def self.inherited(subclass)
|
|
13
|
+
OmniAuth::Strategy.included(subclass)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
args %i[client_id client_secret]
|
|
17
|
+
|
|
18
|
+
option :client_id, nil
|
|
19
|
+
option :client_secret, nil
|
|
20
|
+
option :client_options, {
|
|
21
|
+
:site => "https://app.youneedabudget.com",
|
|
22
|
+
}
|
|
23
|
+
option :authorize_params, {}
|
|
24
|
+
option :authorize_options, %i[scope state]
|
|
25
|
+
option :token_params, {}
|
|
26
|
+
option :token_options, []
|
|
27
|
+
option :auth_token_params, {}
|
|
28
|
+
option :provider_ignores_state, false
|
|
29
|
+
option :pkce, false
|
|
30
|
+
option :pkce_verifier, nil
|
|
31
|
+
option :pkce_options, {
|
|
32
|
+
:code_challenge => proc { |verifier|
|
|
33
|
+
Base64.urlsafe_encode64(
|
|
34
|
+
Digest::SHA2.digest(verifier),
|
|
35
|
+
:padding => false,
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
:code_challenge_method => "S256",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
attr_accessor :access_token
|
|
42
|
+
|
|
43
|
+
def client
|
|
44
|
+
::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
credentials do
|
|
48
|
+
hash = {"token" => access_token.token}
|
|
49
|
+
hash["refresh_token"] = access_token.refresh_token if access_token.expires? && access_token.refresh_token
|
|
50
|
+
hash["expires_at"] = access_token.expires_at if access_token.expires?
|
|
51
|
+
hash["expires"] = access_token.expires?
|
|
52
|
+
hash
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def request_phase
|
|
56
|
+
redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def authorize_params
|
|
60
|
+
options.authorize_params[:state] = SecureRandom.hex(24)
|
|
61
|
+
|
|
62
|
+
if OmniAuth.config.test_mode
|
|
63
|
+
@env ||= {}
|
|
64
|
+
@env["rack.session"] ||= {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
params = options.authorize_params
|
|
68
|
+
.merge(options_for("authorize"))
|
|
69
|
+
.merge(pkce_authorize_params)
|
|
70
|
+
|
|
71
|
+
session["omniauth.pkce.verifier"] = options.pkce_verifier if options.pkce
|
|
72
|
+
session["omniauth.state"] = params[:state]
|
|
73
|
+
|
|
74
|
+
params
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def token_params
|
|
78
|
+
options.token_params.merge(options_for("token")).merge(pkce_token_params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def callback_phase
|
|
82
|
+
if !options.provider_ignores_state && (request.params["state"].to_s.empty? || !secure_compare(request.params["state"], session.delete("omniauth.state")))
|
|
83
|
+
fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
|
|
84
|
+
else
|
|
85
|
+
error = request.params["error_reason"] || request.params["error"]
|
|
86
|
+
if error
|
|
87
|
+
fail!(error, CallbackError.new(request.params["error"], request.params["error_description"] || request.params["error_reason"], request.params["error_uri"]))
|
|
88
|
+
else
|
|
89
|
+
self.access_token = build_access_token
|
|
90
|
+
self.access_token = access_token.refresh! if access_token.expired?
|
|
91
|
+
super
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
rescue ::OAuth2::Error, CallbackError => e
|
|
95
|
+
fail!(:invalid_credentials, e)
|
|
96
|
+
rescue ::Timeout::Error, ::Errno::ETIMEDOUT, ::OAuth2::TimeoutError, ::OAuth2::ConnectionError => e
|
|
97
|
+
fail!(:timeout, e)
|
|
98
|
+
rescue ::SocketError => e
|
|
99
|
+
fail!(:failed_to_connect, e)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
protected
|
|
103
|
+
|
|
104
|
+
def pkce_authorize_params
|
|
105
|
+
return {} unless options.pkce
|
|
106
|
+
|
|
107
|
+
options.pkce_verifier = SecureRandom.hex(64)
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
:code_challenge => options.pkce_options[:code_challenge].call(options.pkce_verifier),
|
|
111
|
+
:code_challenge_method => options.pkce_options[:code_challenge_method],
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def pkce_token_params
|
|
116
|
+
return {} unless options.pkce
|
|
117
|
+
|
|
118
|
+
{:code_verifier => session.delete("omniauth.pkce.verifier")}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_access_token
|
|
122
|
+
verifier = request.params["code"]
|
|
123
|
+
client.auth_code.get_token(
|
|
124
|
+
verifier,
|
|
125
|
+
{:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)),
|
|
126
|
+
deep_symbolize(options.auth_token_params),
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def deep_symbolize(options)
|
|
131
|
+
options.each_with_object({}) do |(key, value), hash|
|
|
132
|
+
hash[key.to_sym] = value.is_a?(Hash) ? deep_symbolize(value) : value
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def options_for(option)
|
|
137
|
+
hash = {}
|
|
138
|
+
options.send(:"#{option}_options").select { |key| options[key] }.each do |key|
|
|
139
|
+
hash[key.to_sym] = if options[key].respond_to?(:call)
|
|
140
|
+
options[key].call(env)
|
|
141
|
+
else
|
|
142
|
+
options[key]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
hash
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Constant-time comparison to prevent timing attacks on state parameter.
|
|
149
|
+
def secure_compare(string_a, string_b)
|
|
150
|
+
return false unless string_a.bytesize == string_b.bytesize
|
|
151
|
+
|
|
152
|
+
l = string_a.unpack("C#{string_a.bytesize}")
|
|
153
|
+
res = 0
|
|
154
|
+
string_b.each_byte { |byte| res |= byte ^ l.shift }
|
|
155
|
+
res.zero?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class CallbackError < StandardError
|
|
159
|
+
attr_accessor :error, :error_reason, :error_uri
|
|
160
|
+
|
|
161
|
+
def initialize(error, error_reason = nil, error_uri = nil)
|
|
162
|
+
self.error = error
|
|
163
|
+
self.error_reason = error_reason
|
|
164
|
+
self.error_uri = error_uri
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def message
|
|
168
|
+
[error, error_reason, error_uri].compact.join(" | ")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
OmniAuth.config.add_camelization "ynab", "YNAB"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require "omniauth-ynab/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |gem|
|
|
6
|
+
gem.add_dependency "oauth2", "~> 2.0"
|
|
7
|
+
gem.add_dependency "omniauth", "~> 2.0"
|
|
8
|
+
|
|
9
|
+
gem.authors = ["tataihono"]
|
|
10
|
+
gem.email = ["tataihono.nikora@gmail.com"]
|
|
11
|
+
gem.description = "A YNAB OAuth2 strategy for OmniAuth."
|
|
12
|
+
gem.summary = gem.description
|
|
13
|
+
gem.homepage = "https://github.com/tataihono/omniauth-v2-ynab"
|
|
14
|
+
gem.licenses = %w[MIT]
|
|
15
|
+
gem.metadata = {"rubygems_mfa_required" => "true"}
|
|
16
|
+
|
|
17
|
+
gem.required_ruby_version = ">= 3.1"
|
|
18
|
+
|
|
19
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").collect { |f| File.basename(f) }
|
|
20
|
+
gem.files = `git ls-files`.split("\n")
|
|
21
|
+
gem.name = "omniauth-v2-ynab"
|
|
22
|
+
gem.require_paths = %w[lib]
|
|
23
|
+
gem.version = OmniAuth::YNAB::VERSION
|
|
24
|
+
end
|
data/spec/helper.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(__dir__)
|
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
3
|
+
|
|
4
|
+
require "rspec"
|
|
5
|
+
require "rack/test"
|
|
6
|
+
require "webmock/rspec"
|
|
7
|
+
require "omniauth"
|
|
8
|
+
require "omniauth-ynab"
|
|
9
|
+
|
|
10
|
+
RSpec.configure do |config|
|
|
11
|
+
config.expect_with :rspec do |c|
|
|
12
|
+
c.syntax = :expect
|
|
13
|
+
end
|
|
14
|
+
config.extend OmniAuth::Test::StrategyMacros, :type => :strategy
|
|
15
|
+
config.include Rack::Test::Methods
|
|
16
|
+
config.include WebMock::API
|
|
17
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
require "helper"
|
|
2
|
+
|
|
3
|
+
describe OmniAuth::Strategies::YNAB do
|
|
4
|
+
def app
|
|
5
|
+
lambda do |_env|
|
|
6
|
+
[200, {}, ["Hello."]]
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
let(:fresh_strategy) { Class.new(OmniAuth::Strategies::YNAB) }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
OmniAuth.config.test_mode = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
after do
|
|
17
|
+
OmniAuth.config.test_mode = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "Subclassing Behavior" do
|
|
21
|
+
subject { fresh_strategy }
|
|
22
|
+
|
|
23
|
+
it "performs the OmniAuth::Strategy included hook" do
|
|
24
|
+
expect(OmniAuth.strategies).to include(OmniAuth::Strategies::YNAB)
|
|
25
|
+
expect(OmniAuth.strategies).to include(subject)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "#client" do
|
|
30
|
+
subject { fresh_strategy }
|
|
31
|
+
|
|
32
|
+
it "is initialized with symbolized client_options" do
|
|
33
|
+
instance = subject.new(app, :client_options => {"authorize_url" => "https://example.com"})
|
|
34
|
+
expect(instance.client.options[:authorize_url]).to eq("https://example.com")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "deep-symbolizes nested client_options" do
|
|
38
|
+
instance = subject.new(app, :client_options => {"connection_opts" => {"timeout" => 30}})
|
|
39
|
+
expect(instance.client.options[:connection_opts]).to eq(:timeout => 30)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "#authorize_params" do
|
|
44
|
+
subject { fresh_strategy }
|
|
45
|
+
|
|
46
|
+
it "includes any authorize params passed in the :authorize_params option" do
|
|
47
|
+
instance = subject.new("abc", "def", :authorize_params => {:foo => "bar", :baz => "zip"})
|
|
48
|
+
expect(instance.authorize_params["foo"]).to eq("bar")
|
|
49
|
+
expect(instance.authorize_params["baz"]).to eq("zip")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "includes top-level options that are marked as :authorize_options" do
|
|
53
|
+
instance = subject.new("abc", "def", :authorize_options => %i[scope foo], :scope => "bar", :foo => "baz")
|
|
54
|
+
expect(instance.authorize_params["scope"]).to eq("bar")
|
|
55
|
+
expect(instance.authorize_params["foo"]).to eq("baz")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "supports callable authorize_options values" do
|
|
59
|
+
instance = subject.new("abc", "def", :authorize_options => [:scope], :scope => proc { "dynamic" })
|
|
60
|
+
expect(instance.authorize_params[:scope]).to eq("dynamic")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "includes a random state parameter and stores it in the session" do
|
|
64
|
+
instance = subject.new("abc", "def")
|
|
65
|
+
params = instance.authorize_params
|
|
66
|
+
expect(params.keys).to include("state")
|
|
67
|
+
expect(instance.session["omniauth.state"]).to eq(params["state"])
|
|
68
|
+
expect(instance.session["omniauth.state"]).not_to be_empty
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context "when pkce is enabled" do
|
|
72
|
+
it "adds code_challenge and code_challenge_method to the params" do
|
|
73
|
+
instance = subject.new("abc", "def", :pkce => true)
|
|
74
|
+
params = instance.authorize_params
|
|
75
|
+
expect(params[:code_challenge]).not_to be_nil
|
|
76
|
+
expect(params[:code_challenge_method]).to eq("S256")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "stores the pkce verifier in the session" do
|
|
80
|
+
instance = subject.new("abc", "def", :pkce => true)
|
|
81
|
+
instance.authorize_params
|
|
82
|
+
expect(instance.session["omniauth.pkce.verifier"]).not_to be_nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "supports a custom code_challenge proc" do
|
|
86
|
+
instance = subject.new("abc", "def",
|
|
87
|
+
:pkce => true,
|
|
88
|
+
:pkce_options => {
|
|
89
|
+
:code_challenge => proc { |_v| "custom_challenge" },
|
|
90
|
+
:code_challenge_method => "plain",
|
|
91
|
+
})
|
|
92
|
+
params = instance.authorize_params
|
|
93
|
+
expect(params[:code_challenge]).to eq("custom_challenge")
|
|
94
|
+
expect(params[:code_challenge_method]).to eq("plain")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe "#token_params" do
|
|
100
|
+
subject { fresh_strategy }
|
|
101
|
+
|
|
102
|
+
it "includes any token params passed in the :token_params option" do
|
|
103
|
+
instance = subject.new("abc", "def", :token_params => {:foo => "bar", :baz => "zip"})
|
|
104
|
+
expect(instance.token_params).to eq("foo" => "bar", "baz" => "zip")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "includes top-level options that are marked as :token_options" do
|
|
108
|
+
instance = subject.new("abc", "def", :token_options => %i[scope foo], :scope => "bar", :foo => "baz")
|
|
109
|
+
expect(instance.token_params).to eq("scope" => "bar", "foo" => "baz")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "includes pkce code_verifier when pkce is enabled" do
|
|
113
|
+
instance = subject.new("abc", "def", :pkce => true)
|
|
114
|
+
instance.authorize_params # populates session verifier
|
|
115
|
+
expect(instance.token_params[:code_verifier]).not_to be_nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe "#callback_phase" do
|
|
120
|
+
subject { fresh_strategy }
|
|
121
|
+
|
|
122
|
+
def stub_session(instance, data = {})
|
|
123
|
+
instance.instance_variable_set(:@env, {"rack.session" => data})
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "calls fail! with the client error when the request contains an error" do
|
|
127
|
+
instance = subject.new("abc", "def")
|
|
128
|
+
stub_session(instance, "omniauth.state" => "abc123")
|
|
129
|
+
allow(instance).to receive(:request) do
|
|
130
|
+
double("Request", :params => {"error_reason" => "user_denied", "error" => "access_denied", "state" => "abc123"})
|
|
131
|
+
end
|
|
132
|
+
expect(instance).to receive(:fail!).with("user_denied", anything)
|
|
133
|
+
instance.callback_phase
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "calls fail! with :csrf_detected when state is missing" do
|
|
137
|
+
instance = subject.new("abc", "def")
|
|
138
|
+
stub_session(instance)
|
|
139
|
+
allow(instance).to receive(:request) do
|
|
140
|
+
double("Request", :params => {"code" => "abc123", "state" => ""})
|
|
141
|
+
end
|
|
142
|
+
expect(instance).to receive(:fail!).with(:csrf_detected, anything)
|
|
143
|
+
instance.callback_phase
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "calls fail! with :csrf_detected when state does not match the session" do
|
|
147
|
+
instance = subject.new("abc", "def")
|
|
148
|
+
stub_session(instance, "omniauth.state" => "correct_state")
|
|
149
|
+
allow(instance).to receive(:request) do
|
|
150
|
+
double("Request", :params => {"code" => "abc123", "state" => "wrong_state"})
|
|
151
|
+
end
|
|
152
|
+
expect(instance).to receive(:fail!).with(:csrf_detected, anything)
|
|
153
|
+
instance.callback_phase
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "checks CSRF state before checking for an error param" do
|
|
157
|
+
instance = subject.new("abc", "def")
|
|
158
|
+
stub_session(instance, "omniauth.state" => "correct_state")
|
|
159
|
+
allow(instance).to receive(:request) do
|
|
160
|
+
double("Request", :params => {"error" => "access_denied", "state" => "wrong_state"})
|
|
161
|
+
end
|
|
162
|
+
expect(instance).to receive(:fail!).with(:csrf_detected, anything)
|
|
163
|
+
instance.callback_phase
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe "#secure_compare" do
|
|
168
|
+
subject { fresh_strategy.new("abc", "def") }
|
|
169
|
+
|
|
170
|
+
it "returns true for identical strings" do
|
|
171
|
+
expect(subject.send(:secure_compare, "foo", "foo")).to be true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "returns false for strings of different length" do
|
|
175
|
+
expect(subject.send(:secure_compare, "foo", "foobar")).to be false
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "returns false for strings of equal length that differ" do
|
|
179
|
+
expect(subject.send(:secure_compare, "foo", "bar")).to be false
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe OmniAuth::Strategies::YNAB::CallbackError do
|
|
185
|
+
describe "#message" do
|
|
186
|
+
it "joins all non-nil attributes with ' | '" do
|
|
187
|
+
instance = described_class.new("error", "description", "uri")
|
|
188
|
+
expect(instance.message).to eq("error | description | uri")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "omits nil attributes" do
|
|
192
|
+
instance = described_class.new(nil, :symbol)
|
|
193
|
+
expect(instance.message).to eq("symbol")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omniauth-v2-ynab
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- tataihono
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: oauth2
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: omniauth
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
description: A YNAB OAuth2 strategy for OmniAuth.
|
|
42
|
+
email:
|
|
43
|
+
- tataihono.nikora@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- ".github/workflows/ci.yml"
|
|
49
|
+
- ".github/workflows/release.yml"
|
|
50
|
+
- ".rspec"
|
|
51
|
+
- ".rubocop.yml"
|
|
52
|
+
- CLAUDE.md
|
|
53
|
+
- Gemfile
|
|
54
|
+
- LICENSE.md
|
|
55
|
+
- README.md
|
|
56
|
+
- Rakefile
|
|
57
|
+
- lib/omniauth-ynab.rb
|
|
58
|
+
- lib/omniauth-ynab/version.rb
|
|
59
|
+
- lib/omniauth/strategies/ynab.rb
|
|
60
|
+
- omniauth-v2-ynab.gemspec
|
|
61
|
+
- spec/helper.rb
|
|
62
|
+
- spec/omniauth/strategies/ynab_spec.rb
|
|
63
|
+
homepage: https://github.com/tataihono/omniauth-v2-ynab
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
post_install_message:
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.1'
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 3.4.20
|
|
84
|
+
signing_key:
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: A YNAB OAuth2 strategy for OmniAuth.
|
|
87
|
+
test_files: []
|