Skip to content

Commit

Permalink
refactor: use UeberauthOidcc as a library
Browse files Browse the repository at this point in the history
  • Loading branch information
paulswartz committed Dec 6, 2023
1 parent ff7f2ec commit a2bf877
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 273 deletions.
187 changes: 96 additions & 91 deletions lib/ueberauth/strategy/google.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@ defmodule Ueberauth.Strategy.Google do
use Ueberauth.Strategy,
uid_field: :sub,
default_scope: "email",
hd: nil,
userinfo_endpoint: "https://www.googleapis.com/oauth2/v3/userinfo"
hd: nil

alias Ueberauth.Auth.Info
alias Ueberauth.Auth.Credentials
alias Ueberauth.Auth.Extra

@session_key "ueberauth_strategy_google"

@doc """
Handles initial request for Google authentication.
"""
def handle_request!(conn) do
scopes = conn.params["scope"] || option(conn, :default_scope)

params =
[scope: scopes]
scopes =
String.split(
conn.params["scope"] || option(conn, :default_scope),
" "
)

scopes =
if "openid" in scopes do
scopes
else
["openid"] ++ scopes
end

authorization_params =
[]
|> with_optional(:hd, conn)
|> with_optional(:prompt, conn)
|> with_optional(:access_type, conn)
Expand All @@ -30,33 +40,39 @@ defmodule Ueberauth.Strategy.Google do
|> with_param(:prompt, conn)
|> with_param(:login_hint, conn)
|> with_param(:hl, conn)
|> with_state_param(conn)

opts = oauth_client_options_from_conn(conn)
redirect!(conn, Ueberauth.Strategy.Google.OAuth.authorize_url!(params, opts))
opts =
conn
|> options_from_conn()
|> Map.put(:scopes, scopes)
|> Map.put(:authorization_params, Map.new(authorization_params))

case UeberauthOidcc.Request.handle_request(opts, conn) do
{:ok, conn} ->
conn

{:error, conn, reason} ->
UeberauthOidcc.Error.set_described_error(conn, reason, "error")
end
end

@doc """
Handles the callback from Google.
"""
def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do
params = [code: code]
opts = oauth_client_options_from_conn(conn)
def handle_callback!(%Plug.Conn{} = conn) do
opts = options_from_conn(conn)

case Ueberauth.Strategy.Google.OAuth.get_access_token(params, opts) do
{:ok, token} ->
fetch_user(conn, token)
case UeberauthOidcc.Callback.handle_callback(opts, conn) do
{:ok, conn, token, userinfo} ->
conn
|> put_private(:google_token, token)
|> put_private(:google_user, userinfo)

{:error, {error_code, error_description}} ->
set_errors!(conn, [error(error_code, error_description)])
{:error, conn, reason} ->
UeberauthOidcc.Error.set_described_error(conn, reason, "error")
end
end

@doc false
def handle_callback!(conn) do
set_errors!(conn, [error("missing_code", "No code received")])
end

@doc false
def handle_cleanup!(conn) do
conn
Expand All @@ -81,87 +97,49 @@ defmodule Ueberauth.Strategy.Google do
"""
def credentials(conn) do
token = conn.private.google_token
scope_string = token.other_params["scope"] || ""
scopes = String.split(scope_string, " ")

%Credentials{
expires: !!token.expires_at,
expires_at: token.expires_at,
scopes: scopes,
token_type: Map.get(token, :token_type),
refresh_token: token.refresh_token,
token: token.access_token
}
credentials = UeberauthOidcc.Auth.credentials(token)
%{credentials | other: %{}}
end

@doc """
Fetches the fields to populate the info section of the `Ueberauth.Auth` struct.
"""
def info(conn) do
token = conn.private.google_token
user = conn.private.google_user

%Info{
email: user["email"],
first_name: user["given_name"],
image: user["picture"],
last_name: user["family_name"],
name: user["name"],
birthday: user["birthday"],
urls: %{
profile: user["profile"],
website: user["hd"]
}
info = UeberauthOidcc.Auth.info(token, user)

%{
info
| birthday: info.birthday || user["birthday"],
urls: Map.put_new(info.urls, :website, user["hd"])
}
end

@doc """
Stores the raw information (including the token) obtained from the google callback.
"""
def extra(conn) do
creds = credentials(conn)

# create a struct with the same format as the old token, even if we don't depend on OAuth2
google_token = %{
__struct__: OAuth2.AccessToken,
access_token: creds.token,
refresh_token: creds.refresh_token,
expires_at: creds.expires_at,
token_type: "Bearer"
}

%Extra{
raw_info: %{
token: conn.private.google_token,
token: google_token,
user: conn.private.google_user
}
}
end

defp fetch_user(conn, token) do
conn = put_private(conn, :google_token, token)

# userinfo_endpoint from https://accounts.google.com/.well-known/openid-configuration
# the userinfo_endpoint may be overridden in options when necessary.
resp = Ueberauth.Strategy.Google.OAuth.get(token, get_userinfo_endpoint(conn))

case resp do
{:ok, %OAuth2.Response{status_code: 401, body: _body}} ->
set_errors!(conn, [error("token", "unauthorized")])

{:ok, %OAuth2.Response{status_code: status_code, body: user}}
when status_code in 200..399 ->
put_private(conn, :google_user, user)

{:error, %OAuth2.Response{status_code: status_code}} ->
set_errors!(conn, [error("OAuth2", status_code)])

{:error, %OAuth2.Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", reason)])
end
end

defp get_userinfo_endpoint(conn) do
case option(conn, :userinfo_endpoint) do
{:system, varname, default} ->
System.get_env(varname) || default

{:system, varname} ->
System.get_env(varname) || Keyword.get(default_options(), :userinfo_endpoint)

other ->
other
end
end

defp with_param(opts, key, conn) do
if value = conn.params[to_string(key)], do: Keyword.put(opts, key, value), else: opts
end
Expand All @@ -170,18 +148,45 @@ defmodule Ueberauth.Strategy.Google do
if option(conn, key), do: Keyword.put(opts, key, option(conn, key)), else: opts
end

defp oauth_client_options_from_conn(conn) do
base_options = [redirect_uri: callback_url(conn)]
request_options = conn.private[:ueberauth_request_options].options
defp options_from_conn(conn) do
base_options = [
issuer: UeberauthGoogle.ProviderConfiguration,
userinfo: true,
session_key: @session_key
]

case {request_options[:client_id], request_options[:client_secret]} do
{nil, _} -> base_options
{_, nil} -> base_options
{id, secret} -> [client_id: id, client_secret: secret] ++ base_options
end
request_options = conn.private[:ueberauth_request_options].options
oauth_options = Application.get_env(:ueberauth, Ueberauth.Strategy.Google.OAuth) || []

[
base_options,
request_options,
oauth_options
]
|> UeberauthOidcc.Config.merge_and_expand_configuration()
|> generate_client_secret()
|> fix_token_url()
end

defp option(conn, key) do
Keyword.get(options(conn), key, Keyword.get(default_options(), key))
end

defp generate_client_secret(%{client_secret: {mod, fun}} = opts) do
Map.put(opts, :client_secret, apply(mod, fun, [Keyword.new(opts)]))
end

defp generate_client_secret(opts) do
opts
end

defp fix_token_url(%{token_url: token_endpoint} = opts) do
opts
|> Map.put(:token_endpoint, token_endpoint)
|> Map.delete(:token_url)
end

defp fix_token_url(opts) do
opts
end
end
106 changes: 0 additions & 106 deletions lib/ueberauth/strategy/google/oauth.ex

This file was deleted.

19 changes: 19 additions & 0 deletions lib/ueberauth_google/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule UeberauthGoogle.Application do
@moduledoc false

use Application

@impl true
def start(_type, _args) do
children = [
{Oidcc.ProviderConfiguration.Worker,
%{
name: UeberauthGoogle.ProviderConfiguration,
issuer: "https://accounts.google.com"
}}
]

opts = [strategy: :one_for_one, name: UeberauthGoogle.Supervisor]
Supervisor.start_link(children, opts)
end
end
Loading

0 comments on commit a2bf877

Please sign in to comment.