Created
May 16, 2024 12:53
-
-
Save jneen/2737b60fd500581952bcb8bc6e61038b to your computer and use it in GitHub Desktop.
OmniAuth strategy for Itch.io
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module OmniAuth | |
module Strategies | |
class ItchIO | |
include OmniAuth::Strategy | |
option :name, 'itch_io' | |
option :client_options, | |
site: 'https://itch.io/', | |
authorize_url: 'https://itch.io/user/oauth' | |
args [:client_id, :client_secret] | |
attr_reader :player, :access_token | |
def client | |
::OAuth2::Client.new(options.client_id, options.client_secret, options.client_options.to_hash.transform_keys(&:to_sym)) | |
end | |
def request_phase | |
redirect client.implicit.authorize_url({ redirect_uri: callback_url }.merge(authorize_params)) | |
end | |
def authorize_params | |
state = SecureRandom.hex(24) | |
session['omniauth.state'] = state | |
{ state: state, scope: 'profile:me' } | |
end | |
# [jneen] This is a quirk of the implicit OAuth2 flow which itch.io | |
# uses. A redirect from an external site can only be a GET request, | |
# and cannot contain any data in the body. This would normally cause | |
# a leak of the client's token to the entire https stack. | |
# | |
# So instead it's passed as a hash param, which doesn't get sent to | |
# the server. We have to serve up a web page that parses this info | |
# out and makes a proper POST request to ourselves. | |
# | |
# As far as I can tell this is the standards-compliant way to do it, | |
# and it's why they recommend only using the Implicit flow for | |
# client-side apps. But this is the only flow implemented by Itch, | |
# so it's what we're stuck with. | |
def bounce_to_javascript | |
[200, {'Content-Type' => 'text/html; charset=utf-8'}, <<~CONTENT] | |
<html> | |
<head> | |
<title>redirecting...</title> | |
</head> | |
<body> | |
<form method="post"> | |
<input type="hidden" name="state" /> | |
<input type="hidden" name="access_token" /> | |
<input type="submit" value="Click here if not redirected..." /> | |
</form> | |
<script type="text/javascript"> | |
var params = new URLSearchParams(window.location.hash.slice(1)); | |
var form = document.getElementsByTagName('form')[0]; | |
form.action = window.location.pathname; | |
form.state.value = params.get('state'); | |
form.access_token.value = params.get('access_token'); | |
form.submit(); | |
</script> | |
</body> | |
</html> | |
CONTENT | |
end | |
def callback_phase | |
return bounce_to_javascript if request.get? | |
error = request.params['error_reason'] || request.params['error'] | |
state = request.params['state'] | |
if !state || state.empty? || state != session.delete('omniauth.state') | |
return fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected")) | |
elsif error | |
err = CallbackError.new( | |
request.params['error'], | |
request.params['error_description'] || request.params['error_reason'], | |
request.params['error_uri'], | |
) | |
return fail!(error, err) | |
end | |
token = request.params['access_token'] | |
fail!(:bad_params, 'missing access_token') unless token | |
@access_token = ::OAuth2::AccessToken.new(client, token) | |
response = @access_token.get('https://itch.io/api/1/key/me') | |
if response.status != 200 | |
return fail!(:invalid_token, CallbackError.new(:invalid_token, "Invalid Token")) | |
end | |
parsed = response.parsed || {} | |
@player = parsed['user'] || {} | |
super | |
end | |
info do | |
{ | |
"nickname" => @player['username'], | |
"url" => @player["url"], | |
"image" => @player["cover_url"], | |
} | |
end | |
extra do | |
@player.slice('gamer', 'press_user', 'developer') | |
end | |
uid do | |
@player['id'] | |
end | |
class CallbackError < StandardError | |
attr_accessor :error, :error_reason, :error_uri | |
def initialize(error, error_reason = nil, error_uri = nil) | |
self.error = error | |
self.error_reason = error_reason | |
self.error_uri = error_uri | |
end | |
def message | |
[error, error_reason, error_uri].compact.join(" | ") | |
end | |
end | |
end | |
end | |
end | |
OmniAuth.config.add_camelization 'itch_io', 'ItchIO' | |
OmniAuth.config.add_camelization 'itchio', 'ItchIO' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment