Initial commit

This commit is contained in:
Evert Prants 2020-06-01 17:41:37 +03:00
commit 941652e0f4
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
11 changed files with 516 additions and 0 deletions

59
.gitignore vendored Normal file
View File

@ -0,0 +1,59 @@
# Created by https://www.gitignore.io/api/elixir,osx,vim
### Elixir ###
/_build
/cover
/deps
erl_crash.dump
*.ez
*.beam
### Elixir Patch ###
/doc
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Vim ###
# swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
# End of https://www.gitignore.io/api/elixir,osx,vim
### VS Code ###
.elixir_ls

22
.travis.yml Normal file
View File

@ -0,0 +1,22 @@
language: elixir
elixir:
- 1.5
- 1.4
otp_release:
- 20.0
- 19.3
env:
global:
- MIX_ENV=test
notifications:
email: false
sudo: false
script:
- mix test
- mix credo

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Überauth IcyNet
> Icy Network OAuth2 strategy for Überauth.

30
config/config.exs Normal file
View File

@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.
# You can configure for your application as:
#
# config :ueberauth_icynet, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:ueberauth_icynet, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"

3
lib/ueber_icynet.ex Normal file
View File

@ -0,0 +1,3 @@
defmodule UeberauthIcyNet do
@moduledoc false
end

View File

@ -0,0 +1,202 @@
defmodule Ueberauth.Strategy.IcyNet do
@moduledoc """
Provides an Ueberauth strategy for authenticating with Icy Network.
### Setup
Obtain a Client ID and Secret from Icy Network.
Include the provider in your configuration for Ueberauth
config :ueberauth, Ueberauth,
providers: [
icynet: { Ueberauth.Strategy.IcyNet, [] }
]
Then include the configuration for icynet.
config :ueberauth, Ueberauth.Strategy.IcyNet.OAuth,
client_id: System.get_env("ICYNET_CLIENT_ID"),
client_secret: System.get_env("ICYNET_CLIENT_SECRET")
If you haven't already, create a pipeline and setup routes for your callback handler
pipeline :auth do
Ueberauth.plug "/auth"
end
scope "/auth" do
pipe_through [:browser, :auth]
get "/:provider/callback", AuthController, :callback
end
Create an endpoint for the callback where you will handle the `Ueberauth.Auth` struct
defmodule MyApp.AuthController do
use MyApp.Web, :controller
def callback_phase(%{ assigns: %{ ueberauth_failure: fails } } = conn, _params) do
# do things with the failure
end
def callback_phase(%{ assigns: %{ ueberauth_auth: auth } } = conn, params) do
# do things with the auth
end
end
You can edit the behaviour of the Strategy by including some options when you register your provider.
To set the `uid_field`
config :ueberauth, Ueberauth,
providers: [
icynet: { Ueberauth.Strategy.IcyNet, [uid_field: :email] }
]
Default is `:id`
To set the default 'scopes' (permissions):
config :ueberauth, Ueberauth,
providers: [
icynet: { Ueberauth.Strategy.IcyNet, [default_scope: "email,image"] }
]
Default is empty ("") which "Grants read-only access to public information (includes public user profile info, public repository info, and gists)"
"""
use Ueberauth.Strategy,
uid_field: :id,
default_scope: "",
oauth2_module: Ueberauth.Strategy.IcyNet.OAuth
alias Ueberauth.Auth.Info
alias Ueberauth.Auth.Credentials
alias Ueberauth.Auth.Extra
@doc """
Handles the initial redirect to the icynet authentication page.
To customize the scope (permissions) that are requested by icynet include them as part of your url:
"/auth/icynet?scope=email,image,privilege"
You can also include a `state` param that icynet will return to you.
"""
def handle_request!(conn) do
scopes = conn.params["scope"] || option(conn, :default_scope)
send_redirect_uri = Keyword.get(options(conn), :send_redirect_uri, true)
opts =
if send_redirect_uri do
[redirect_uri: callback_url(conn), scope: scopes]
else
[scope: scopes]
end
opts =
if conn.params["state"], do: Keyword.put(opts, :state, conn.params["state"]), else: opts
module = option(conn, :oauth2_module)
redirect!(conn, apply(module, :authorize_url!, [opts]))
end
@doc """
Handles the callback from IcyNet. When there is a failure from IcyNet the failure is included in the
`ueberauth_failure` struct. Otherwise the information returned from IcyNet is returned in the `Ueberauth.Auth` struct.
"""
def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do
module = option(conn, :oauth2_module)
token = apply(module, :get_token!, [[code: code]])
if token.access_token == nil do
set_errors!(conn, [
error(token.other_params["error"], token.other_params["error_description"])
])
else
fetch_user(conn, token)
end
end
@doc false
def handle_callback!(conn) do
set_errors!(conn, [error("missing_code", "No code received")])
end
@doc """
Cleans up the private area of the connection used for passing the raw IcyNet response around during the callback.
"""
def handle_cleanup!(conn) do
conn
|> put_private(:icynet_user, nil)
|> put_private(:icynet_token, nil)
end
@doc """
Fetches the uid field from the IcyNet response. This defaults to the option `uid_field` which in-turn defaults to `id`
"""
def uid(conn) do
conn |> option(:uid_field) |> to_string()
end
@doc """
Includes the credentials from the IcyNet response.
"""
def credentials(conn) do
token = conn.private.icynet_token
%Credentials{
token: token.access_token,
refresh_token: token.refresh_token,
expires_at: token.expires_at,
token_type: token.token_type,
expires: !!token.expires_in
}
end
@doc """
Fetches the fields to populate the info section of the `Ueberauth.Auth` struct.
"""
def info(conn) do
user = conn.private.icynet_user
%Info{
name: user["username"],
nickname: user["display_name"],
email: user["email"],
image: user["image"],
}
end
@doc """
Stores the raw information (including the token) obtained from the IcyNet callback.
"""
def extra(conn) do
%Extra{
raw_info: %{
token: conn.private.icynet_token,
user: conn.private.icynet_user
}
}
end
defp fetch_user(conn, token) do
conn = put_private(conn, :icynet_token, token)
case Ueberauth.Strategy.IcyNet.OAuth.get(token, "/oauth2/user") 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, :icynet_user, user)
{:error, %OAuth2.Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", reason)])
end
end
defp option(conn, key) do
Keyword.get(options(conn), key, Keyword.get(default_options(), key))
end
end

View File

@ -0,0 +1,117 @@
defmodule Ueberauth.Strategy.IcyNet.OAuth do
@moduledoc """
An implementation of OAuth2 for icynet.
To add your `client_id` and `client_secret` include these values in your configuration.
config :ueberauth, Ueberauth.Strategy.IcyNet.OAuth,
client_id: System.get_env("ICYNET_CLIENT_ID"),
client_secret: System.get_env("ICYNET_CLIENT_SECRET")
"""
use OAuth2.Strategy
@defaults [
strategy: __MODULE__,
site: "https://icynet.eu",
authorize_url: "https://icynet.eu/oauth2/authorize",
token_url: "https://icynet.eu/oauth2/token",
token_method: :post
]
@doc """
Construct a client for requests to IcyNet.
Optionally include any OAuth2 options here to be merged with the defaults.
Ueberauth.Strategy.IcyNet.OAuth.client(redirect_uri: "http://localhost:4000/auth/icynet/callback")
This will be setup automatically for you in `Ueberauth.Strategy.IcyNet`.
These options are only useful for usage outside the normal callback phase of Ueberauth.
"""
def client(opts \\ []) do
config =
:ueberauth
|> Application.fetch_env!(Ueberauth.Strategy.IcyNet.OAuth)
|> check_credential(:client_id)
|> check_credential(:client_secret)
client_opts =
@defaults
|> Keyword.merge(config)
|> Keyword.merge(opts)
json_library = Ueberauth.json_library()
OAuth2.Client.new(client_opts)
|> OAuth2.Client.put_serializer("application/json", json_library)
end
@doc """
Provides the authorize url for the request phase of Ueberauth. No need to call this usually.
"""
def authorize_url!(params \\ [], opts \\ []) do
opts
|> client
|> OAuth2.Client.authorize_url!(params)
end
def get(token, url, headers \\ [], opts \\ []) do
[token: token]
|> client
|> put_param("access_token", token)
|> OAuth2.Client.get(url, headers, opts)
end
def get_token!(params \\ [], options \\ []) do
headers = Keyword.get(options, :headers, [])
options = Keyword.get(options, :options, [])
client_options = Keyword.get(options, :client_options, [])
client = OAuth2.Client.get_token!(client(client_options), params, headers, options)
client.token
end
# Strategy Callbacks
def authorize_url(client, params) do
client
|> put_param("response_type", "code")
|> put_param("redirect_uri", client().redirect_uri)
OAuth2.Strategy.AuthCode.authorize_url(client, params)
end
def get_token(client, params, headers) do
client
|> put_param("client_id", client.client_id)
|> put_param("client_secret", client.client_secret)
|> put_param("grant_type", "authorization_code")
|> put_param("redirect_uri", client().redirect_uri)
|> put_header("Accept", "application/json")
|> OAuth2.Strategy.AuthCode.get_token(params, headers)
end
defp check_credential(config, key) do
check_config_key_exists(config, key)
case Keyword.get(config, key) do
value when is_binary(value) ->
config
{:system, env_key} ->
case System.get_env(env_key) do
nil ->
raise "#{inspect (env_key)} missing from environment, expected in config :ueberauth, Ueberauth.Strategy.IcyNet"
value ->
Keyword.put(config, key, value)
end
end
end
defp check_config_key_exists(config, key) when is_list(config) do
unless Keyword.has_key?(config, key) do
raise "#{inspect (key)} missing from config :ueberauth, Ueberauth.Strategy.IcyNet"
end
config
end
defp check_config_key_exists(_, _) do
raise "Config :ueberauth, Ueberauth.Strategy.IcyNet is not a keyword list, as expected"
end
end

52
mix.exs Normal file
View File

@ -0,0 +1,52 @@
defmodule Ueberauth.IcyNet.Mixfile do
use Mix.Project
@version "0.0.1"
def project do
[app: :ueberauth_icynet,
version: @version,
name: "Ueberauth IcyNet",
package: package(),
elixir: "~> 1.3",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
source_url: "https://gitlab.icynet.eu/IcyNetwork/ueberauth_icynet",
homepage_url: "https://gitlab.icynet.eu/IcyNetwork/ueberauth_icynet",
description: description(),
deps: deps(),
docs: docs()]
end
def application do
[applications: [:logger, :ueberauth, :oauth2]]
end
defp deps do
[
{:oauth2, "~> 1.0 or ~> 2.0"},
{:ueberauth, "~> 0.6.0"},
# dev/test only dependencies
{:credo, "~> 0.8", only: [:dev, :test]},
# docs dependencies
{:ex_doc, ">= 0.0.0", only: :dev}
]
end
defp docs do
[extras: ["README.md"]]
end
defp description do
"An Ueberauth strategy for using IcyNet to authenticate your users."
end
defp package do
[files: ["lib", "mix.exs", "README.md"],
maintainers: ["Evert Prants"],
licenses: ["CC0"],
links: %{"Gitlab": "https://gitlab.icynet.eu/IcyNetwork/ueberauth_icynet"}]
end
end

23
mix.lock Normal file
View File

@ -0,0 +1,23 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "539596b6774069260d5938aa73042a2f5157e1c0215aa35f5a53d83889546d14"},
"earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"},
"ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"makeup": {:hex, :makeup, "1.0.2", "0b9f7bfb7a88bed961341b359bc2cc1b233517af891ba4890ec5a580ffe738b4", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "43833299231c6a6983afc75a34e43eeba638521d5527ff89809fa6372424fd7e"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}

1
test/test_helper.exs Normal file
View File

@ -0,0 +1 @@
ExUnit.start()

View File

@ -0,0 +1,4 @@
defmodule UeberauthIcyNetTest do
use ExUnit.Case
doctest UeberauthIcyNet
end