Refactor to use OpenID Connect OmniAuth gem instead of plain OAuth2

Right, I didn't totally connect the dots that there's some OpenID
features in the mix here for how we expect to identify the user once
they authenticate. It requires looking up the provider's public key,
and validating the JWT they sent us. This gem does all that for us!

I don't actually know what a real NeoPass `id_token` looks like yet?
But I'll fill in some placeholder stuff for now, and use that for
initializing the account!
This commit is contained in:
Emi Matchu 2024-03-14 18:11:40 -07:00
parent ffcfce2eb8
commit 9cbeee0acd
26 changed files with 89 additions and 88 deletions

View file

@ -27,7 +27,7 @@ gem 'devise', '~> 4.9', '>= 4.9.2'
gem 'devise-encryptable', '~> 0.2.0' gem 'devise-encryptable', '~> 0.2.0'
gem 'omniauth', '~> 2.1' gem 'omniauth', '~> 2.1'
gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-oauth2', '~> 1.8' gem "omniauth_openid_connect", "~> 0.7.1"
# For pagination UI. # For pagination UI.
gem 'will_paginate', '~> 4.0' gem 'will_paginate', '~> 4.0'

View file

@ -83,6 +83,7 @@ GEM
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.6) addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
async (2.8.1) async (2.8.1)
console (~> 1.10) console (~> 1.10)
fiber-annotation fiber-annotation
@ -105,6 +106,7 @@ GEM
async async
async-pool (0.4.0) async-pool (0.4.0)
async (>= 1.25) async (>= 1.25)
attr_required (1.0.2)
babel-source (5.8.35) babel-source (5.8.35)
babel-transpiler (0.7.0) babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6) babel-source (>= 4.0, < 6)
@ -112,6 +114,7 @@ GEM
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
bigdecimal (3.1.6) bigdecimal (3.1.6)
bindata (2.5.0)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.3) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
@ -140,6 +143,8 @@ GEM
drb (2.2.0) drb (2.2.0)
ruby2_keywords ruby2_keywords
e2mmap (0.1.0) e2mmap (0.1.0)
email_validator (2.2.4)
activemodel
erubi (1.12.0) erubi (1.12.0)
execjs (2.9.1) execjs (2.9.1)
falcon (0.43.0) falcon (0.43.0)
@ -157,6 +162,8 @@ GEM
samovar (~> 2.1) samovar (~> 2.1)
faraday (2.9.0) faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2) faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-net_http (3.1.0) faraday-net_http (3.1.0)
net-http net-http
ffi (1.16.3) ffi (1.16.3)
@ -183,8 +190,13 @@ GEM
jsbundling-rails (1.3.0) jsbundling-rails (1.3.0)
railties (>= 6.0.0) railties (>= 6.0.0)
json (2.7.1) json (2.7.1)
jwt (2.8.1) json-jwt (1.16.6)
activesupport (>= 4.2)
aes_key_wrap
base64 base64
bindata
faraday (~> 2.0)
faraday-follow_redirects
launchy (2.5.2) launchy (2.5.2)
addressable (~> 2.8) addressable (~> 2.8)
letter_opener (1.9.0) letter_opener (1.9.0)
@ -223,23 +235,29 @@ GEM
nokogiri (1.16.2) nokogiri (1.16.2)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oauth2 (2.0.9)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
omniauth (2.1.2) omniauth (2.1.2)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
omniauth-rails_csrf_protection (1.0.1) omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2) actionpack (>= 4.2)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth_openid_connect (0.7.1)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
openid_connect (2.3.0)
activemodel
attr_required (>= 1.0.0)
email_validator
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
mail
rack-oauth2 (~> 2.2)
swd (~> 2.0)
tzinfo
validate_url
webfinger (~> 2.0)
openssl (3.2.0) openssl (3.2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
parallel (1.24.0) parallel (1.24.0)
@ -265,6 +283,13 @@ GEM
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-mini-profiler (3.3.1) rack-mini-profiler (3.3.1)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-oauth2 (2.2.1)
activesupport
attr_required
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (4.0.0) rack-protection (4.0.0)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (>= 3.0.0, < 4) rack (>= 3.0.0, < 4)
@ -349,9 +374,6 @@ GEM
shell (0.8.1) shell (0.8.1)
e2mmap e2mmap
sync sync
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
sprockets (4.2.1) sprockets (4.2.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4) rack (>= 2.2.4, < 4)
@ -363,6 +385,11 @@ GEM
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
stackprof (0.2.26) stackprof (0.2.26)
stringio (3.1.0) stringio (3.1.0)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
sync (0.5.0) sync (0.5.0)
temple (0.10.3) temple (0.10.3)
terser (1.2.0) terser (1.2.0)
@ -380,7 +407,9 @@ GEM
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
uri (0.13.0) uri (0.13.0)
version_gem (1.1.3) validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@ -388,6 +417,10 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webfinger (2.1.3)
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webrick (1.8.1) webrick (1.8.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
@ -417,8 +450,8 @@ DEPENDENCIES
mysql2 (~> 0.5.5) mysql2 (~> 0.5.5)
nokogiri (~> 1.15, >= 1.15.3) nokogiri (~> 1.15, >= 1.15.3)
omniauth (~> 2.1) omniauth (~> 2.1)
omniauth-oauth2 (~> 1.8)
omniauth-rails_csrf_protection (~> 1.0) omniauth-rails_csrf_protection (~> 1.0)
omniauth_openid_connect (~> 0.7.1)
parallel (~> 1.23) parallel (~> 1.23)
rack-attack (~> 6.7) rack-attack (~> 6.7)
rack-mini-profiler (~> 3.1) rack-mini-profiler (~> 3.1)

View file

@ -3,14 +3,14 @@
* A test NeoPass server! This is a very lean, hacky implementation, designed * A test NeoPass server! This is a very lean, hacky implementation, designed
* to just see the basic OAuth interactions Work At All. * to just see the basic OAuth interactions Work At All.
* *
* First, we have a "backing server", which is a `oauth2-mock-server` instance * This server is an `oauth2-mock-server` instance that's easy to spin up and
* that's easy to spin up and have perform OAuth for us. We give it a hardcoded * have perform OAuth for us. We give it a hardcoded development-only key, and
* development-only key, and it just auto-grants permissions! * it just auto-grants permissions!
* *
* We also have a "main server", which obeys the actual NeoPass API: the * It slightly differs from the NeoPass spec, in that it uses different paths
* backing server isn't configurable with stuff like paths, so we use the main * for its endpoints, but that's okay: DTI will use OpenID's "discovery"
* server to proxy from the paths in the NeoPass spec to the paths the backing * feature to discover those endpoints via a single well-known path, without
* server uses. * needing them hardcoded.
*/ */
const fs = require("node:fs/promises"); const fs = require("node:fs/promises");
@ -67,7 +67,7 @@ async function ensureCertsExist() {
} }
} }
async function startBackingServer(port) { async function startServer(port) {
const server = new OAuth2Server( const server = new OAuth2Server(
keyPath, keyPath,
certPath, certPath,
@ -90,53 +90,12 @@ async function startBackingServer(port) {
}); });
await server.start(port, "localhost"); await server.start(port, "localhost");
console.log(`Started NeoPass backing server at: ${server.issuer.url}`); console.log(`Started NeoPass development server at: ${server.issuer.url}`);
}
async function startMainServer(port) {
const fetch = (await import("node-fetch")).default;
const app = express();
app.use(express.text({ type: "*/*" }));
app.get("/", (req, res) => res.end("NeoPass development server for DTI!"));
app.get("/oauth2/auth", (req, res) => {
const query = urlLib.parse(req.url).query;
res.redirect(`http://localhost:8686/authorize?${query}`);
});
app.post("/oauth2/token", async (req, res) => {
try {
// For POST requests, the HTTP spec doesn't allow a redirect to a
// POST, so we proxy the request instead.
const backingRes = await fetch("http://localhost:8686/token", {
method: "POST",
headers: {
"Content-Type": req.get("Content-Type"),
},
body: req.body,
});
if (!backingRes.ok) {
throw new Error(`backing server returned status ${res.status}`);
}
res.set("Content-Type", backingRes.headers.get("Content-Type"));
return res.end(await backingRes.text());
} catch (error) {
console.error(error);
return res.end(error.message);
}
});
await new Promise((resolve) => app.listen(port, resolve));
console.log(`Started NeoPass main server at: http://localhost:${port}`);
} }
async function main() { async function main() {
await ensureCertsExist(); await ensureCertsExist();
await startBackingServer(8686); await startServer(8585);
await startMainServer(8585);
} }
main().catch((error) => { main().catch((error) => {

View file

@ -121,5 +121,9 @@ Rails.application.configure do
config.neopass_access_secret = "1" config.neopass_access_secret = "1"
# Use the local NeoPass development server. # Use the local NeoPass development server.
config.neopass_origin = "http://localhost:8585" config.neopass_origin = "https://localhost:8585"
# Set the NeoPass redirect callback URL.
config.neopass_redirect_uri =
"http://localhost:3000/auth_users/auth/neopass/callback"
end end

View file

@ -140,4 +140,8 @@ Rails.application.configure do
# Use the live NeoPass production server. # Use the live NeoPass production server.
config.neopass_origin = "https://oidc.neopets.com" config.neopass_origin = "https://oidc.neopets.com"
# Set the NeoPass redirect callback URL.
config.neopass_redirect_uri =
"https://impress.openneo.net/auth_users/auth/neopass/callback"
end end

View file

@ -76,5 +76,9 @@ Rails.application.configure do
config.neopass_access_secret = "1" config.neopass_access_secret = "1"
# Use the local NeoPass development server. # Use the local NeoPass development server.
config.neopass_origin = "http://localhost:8585" config.neopass_origin = "https://localhost:8585"
# Set the NeoPass redirect callback URL.
config.neopass_redirect_uri =
"http://localhost:3000/auth_users/auth/neopass/callback"
end end

View file

@ -1,5 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
require "strategies/neopass"
# Assuming you have not yet modified this file, each configuration option below # Assuming you have not yet modified this file, each configuration option below
# is set to its default value. Note that some are commented out while others # is set to its default value. Note that some are commented out while others
@ -274,7 +273,21 @@ Devise.setup do |config|
# ==> OmniAuth # ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting # Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks. # up on your models and hooks.
config.omniauth :neopass, strategy_class: Strategies::NeoPass config.omniauth :openid_connect, {
name: :neopass,
scope: [:openid, :email, :profile],
response_type: :code,
issuer: Rails.configuration.neopass_origin,
discovery: true,
client_options: {
identifier: "DTI-TODO",
secret: "DTI-TODO",
redirect_uri: Rails.configuration.neopass_redirect_uri,
},
}
# Output OmniAuth debug info to the server logs in development
OmniAuth.config.logger = Rails.logger if Rails.env.development?
# ==> Warden configuration # ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or # If you want to use other strategies, that are not supported by Devise, or

View file

@ -18,7 +18,4 @@
ActiveSupport::Inflector.inflections(:en) do |inflect| ActiveSupport::Inflector.inflections(:en) do |inflect|
# Teach Zeitwerk that `RocketAMF` is what to expect in `lib/rocketamf`. # Teach Zeitwerk that `RocketAMF` is what to expect in `lib/rocketamf`.
inflect.acronym "RocketAMF" inflect.acronym "RocketAMF"
# Teach Zeitwerk that "NeoPass" is what to expect in `neopass.rb`.
inflect.acronym "NeoPass"
end end

View file

@ -1,13 +0,0 @@
require "omniauth-oauth2"
module Strategies
class NeoPass < OmniAuth::Strategies::OAuth2
option :name, "neopass"
option :client_options, {
site: Rails.configuration.neopass_origin,
authorize_url: "/oauth2/auth",
token_url: "/oauth2/token",
}
end
end

BIN
vendor/cache/aes_key_wrap-1.1.0.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/attr_required-1.0.2.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/bindata-2.5.0.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/email_validator-2.2.4.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/json-jwt-1.16.6.gem vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/openid_connect-2.3.0.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/rack-oauth2-2.2.1.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/swd-2.0.3.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/validate_url-1.0.15.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/webfinger-2.1.3.gem vendored Normal file

Binary file not shown.