Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use UeberauthOidcc as a library #105

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.11'
otp-version: '22.3'
elixir-version: '1.14.4'
otp-version: '26.1.2'

- name: Install Dependencies
run: |
Expand All @@ -36,8 +36,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.11'
otp-version: '22.3'
elixir-version: '1.14.4'
otp-version: '26.1.2'

- name: Install Dependencies
run: |
Expand Down
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.

Loading
Loading