├── test ├── test_helper.exs ├── doc │ └── doc_test.exs ├── helpers │ └── helpers_test.exs ├── static_qr │ └── static_qr_test.exs ├── cancel │ └── client_test.exs ├── sign │ └── client_test.exs ├── auth │ └── client_test.exs ├── collect │ └── client_test.exs └── json │ └── json_default_test.exs ├── .formatter.exs ├── lib ├── ex_bank_id │ ├── collect │ │ ├── user.ex │ │ ├── response.ex │ │ ├── completion_data.ex │ │ └── payload.ex │ ├── error │ │ └── api.ex │ ├── http │ │ ├── response.ex │ │ ├── default │ │ │ └── httpoison.ex │ │ └── client.ex │ ├── auth │ │ ├── response.ex │ │ └── payload.ex │ ├── sign │ │ ├── response.ex │ │ └── payload.ex │ ├── qr.ex │ ├── json │ │ ├── handler.ex │ │ └── default │ │ │ └── poison.ex │ ├── cancel │ │ └── payload.ex │ ├── auth.ex │ ├── cancel.ex │ ├── collect.ex │ ├── sign.ex │ ├── http_request.ex │ └── payload_helpers.ex └── ex_bank_id.ex ├── .gitignore ├── .github └── workflows │ └── elixir.yml ├── LICENSE ├── assets ├── test_cacert.cer └── test.pem ├── mix.exs ├── README.md ├── .credo.exs └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Code.require_file("./test/helpers/helpers_test.exs") 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /lib/ex_bank_id/collect/user.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Collect.User do 2 | defstruct givenName: nil, name: nil, personalNumber: nil, surname: nil 3 | end 4 | -------------------------------------------------------------------------------- /lib/ex_bank_id/collect/response.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Collect.Response do 2 | alias ExBankID.Collect.CompletionData 3 | defstruct completionData: %CompletionData{}, orderRef: nil, status: nil, hintCode: nil 4 | end 5 | -------------------------------------------------------------------------------- /lib/ex_bank_id/error/api.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Error.Api do 2 | @moduledoc """ 3 | Provides the struct use to represent a response from the BankID api that has http code 400 - 500. 4 | """ 5 | defstruct errorCode: nil, details: nil 6 | end 7 | -------------------------------------------------------------------------------- /lib/ex_bank_id/http/response.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Http.Response do 2 | @doc false 3 | @type t() :: %__MODULE__{status_code: pos_integer(), body: String.t()} 4 | @enforce_keys [:status_code, :body] 5 | defstruct [:status_code, :body] 6 | end 7 | -------------------------------------------------------------------------------- /test/doc/doc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Auth.PayloadTest do 2 | use ExUnit.Case, async: true 3 | doctest ExBankID.Auth.Payload 4 | doctest ExBankID.Sign.Payload 5 | doctest ExBankID.Cancel.Payload 6 | doctest ExBankID.PayloadHelpers 7 | end 8 | -------------------------------------------------------------------------------- /lib/ex_bank_id/collect/completion_data.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Collect.CompletionData do 2 | alias ExBankID.Collect.User 3 | 4 | defstruct( 5 | user: %User{}, 6 | device: %{}, 7 | cert: %{}, 8 | signature: nil, 9 | ocspResponse: nil 10 | ) 11 | end 12 | -------------------------------------------------------------------------------- /lib/ex_bank_id/auth/response.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Auth.Response do 2 | @moduledoc """ 3 | Provides the struct used to represent the response from a successful request to the BankID /auth endpoint 4 | """ 5 | defstruct [:orderRef, :autoStartToken, :qrStartToken, :qrStartSecret] 6 | end 7 | -------------------------------------------------------------------------------- /lib/ex_bank_id/sign/response.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Sign.Response do 2 | @moduledoc """ 3 | Provides the struct used to represent the response from a successful request to the BankID /sign endpoint 4 | """ 5 | defstruct [:orderRef, :autoStartToken, :qrStartToken, :qrStartSecret] 6 | end 7 | -------------------------------------------------------------------------------- /lib/ex_bank_id/qr.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Qr do 2 | @static_token_prefix "bankid:///?autostarttoken=" 3 | 4 | def static_qr(%ExBankID.Auth.Response{autoStartToken: token}) do 5 | @static_token_prefix <> token 6 | end 7 | 8 | def static_qr(%ExBankID.Sign.Response{autoStartToken: token}) do 9 | @static_token_prefix <> token 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/helpers/helpers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Helpers do 2 | def get_url(port), do: "http://localhost:#{port}" 3 | 4 | def endpoint_handler(http_code, response, expected_request_payload) do 5 | fn conn -> 6 | {:ok, body, _} = Plug.Conn.read_body(conn) 7 | {:ok, ^expected_request_payload} = Poison.decode(body) 8 | 9 | Plug.Conn.resp( 10 | conn, 11 | http_code, 12 | response 13 | ) 14 | end 15 | end 16 | 17 | def cert_file(), do: __DIR__ <> "../../assets/test.pem" 18 | end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | bankid_http_client-*.tar 24 | 25 | /config/ 26 | 27 | *.todo -------------------------------------------------------------------------------- /lib/ex_bank_id/json/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Json.Handler do 2 | @moduledoc """ 3 | The expected behaviour a module capable of encoding and decoding json 4 | """ 5 | 6 | @doc """ 7 | An implementation of this should decode the json string into the given struct. 8 | Keys missing in the json string should not result in an error. 9 | """ 10 | @callback decode(json :: String.t(), target :: struct()) :: {:ok, struct()} | {:error, String.t()} 11 | 12 | @doc """ 13 | An implementation of this should decode the given json string to the equivalent elixir value. 14 | """ 15 | @callback decode(json :: String.t()) :: {:ok, any} | {:error, String.t()} 16 | 17 | @callback encode!(payload :: struct()) :: String.t() 18 | end 19 | -------------------------------------------------------------------------------- /lib/ex_bank_id/http/default/httpoison.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(HTTPoison) do 2 | defmodule ExBankID.Http.Default do 3 | @moduledoc """ 4 | HTTPoison implementation of the ExBankID.Http.Client behaviour. 5 | """ 6 | 7 | @behaviour ExBankID.Http.Client 8 | alias ExBankID.Http.Response 9 | 10 | def post(url, req_body, headers, cert_file, http_opts \\ []) do 11 | opts = Keyword.put(http_opts, :ssl, certfile: cert_file) 12 | 13 | case HTTPoison.post(url, req_body, headers, opts) do 14 | {:ok, %HTTPoison.Response{status_code: code, body: body}} -> 15 | {:ok, %Response{status_code: code, body: body}} 16 | 17 | {:error, %HTTPoison.Error{reason: reason}} -> 18 | {:error, reason} 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/ex_bank_id/collect/payload.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Collect.Payload do 2 | defstruct [:orderRef] 3 | 4 | def new(%ExBankID.Auth.Response{orderRef: order_ref}) when is_binary(order_ref) do 5 | %ExBankID.Collect.Payload{} 6 | |> set_order_ref(order_ref) 7 | end 8 | 9 | def new(%ExBankID.Sign.Response{orderRef: order_ref}) when is_binary(order_ref) do 10 | %ExBankID.Collect.Payload{} 11 | |> set_order_ref(order_ref) 12 | end 13 | 14 | def new(order_ref) when is_binary(order_ref) do 15 | %ExBankID.Collect.Payload{} 16 | |> set_order_ref(order_ref) 17 | end 18 | 19 | defp set_order_ref(payload, order_ref) do 20 | case UUID.info(order_ref) do 21 | {:ok, _} -> 22 | %{payload | orderRef: order_ref} 23 | 24 | _ -> 25 | {:error, "OrderRef is not a valid UUID"} 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | name: Build and test 13 | runs-on: ubuntu-latest 14 | env: 15 | MIX_ENV: test 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Elixir 20 | uses: actions/setup-elixir@v1 21 | with: 22 | elixir-version: '1.10.3' # Define the elixir version [required] 23 | otp-version: '22.3' # Define the OTP version [required] 24 | - name: Restore dependencies cache 25 | uses: actions/cache@v2 26 | with: 27 | path: deps 28 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 29 | restore-keys: ${{ runner.os }}-mix- 30 | - name: Install dependencies 31 | run: mix deps.get 32 | - name: Run tests 33 | run: mix coveralls.github 34 | -------------------------------------------------------------------------------- /lib/ex_bank_id/http/client.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Http.Client do 2 | @moduledoc """ 3 | The expected behaviour an http client 4 | 5 | You can provide your own http client by creating a module that implements this behaviour. 6 | Things to consider: 7 | 1. The HTTP method must be POST. 8 | 2. cert_file will be the path to a pem file used to authenticate with the BankID API. Corresponds to [certfile](http://erlang.org/doc/man/ssl.html#TLS/DTLS%20OPTION%20DESCRIPTIONS%20-%20COMMON%20for%20SERVER%20and%20CLIENT) 9 | 10 | 11 | The default implementation can be found in ExBankID.Http.Default 12 | """ 13 | 14 | alias ExBankID.Http.Response 15 | 16 | @callback post( 17 | url :: binary, 18 | req_body :: binary, 19 | headers :: [{binary, binary}, ...], 20 | cert_file :: String.t(), 21 | http_opts :: term 22 | ) :: 23 | {:ok, Response.t()} 24 | | {:error, String.t()} 25 | end 26 | -------------------------------------------------------------------------------- /test/static_qr/static_qr_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Static.Qr do 2 | use ExUnit.Case, async: true 3 | 4 | test "Static qr-code from auth response" do 5 | response = %ExBankID.Auth.Response{ 6 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 7 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 8 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 9 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 10 | } 11 | 12 | assert "bankid:///?autostarttoken=7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6" = 13 | ExBankID.static_qr(response) 14 | end 15 | 16 | test "Static qr-code from sign response" do 17 | response = %ExBankID.Sign.Response{ 18 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 19 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 20 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 21 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 22 | } 23 | 24 | assert "bankid:///?autostarttoken=7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6" = 25 | ExBankID.static_qr(response) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Viktor Hellström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/ex_bank_id/json/default/poison.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Poison) do 2 | defmodule ExBankID.Json.Default do 3 | @moduledoc """ 4 | This is the default implementation of ExBankID.Json.Handler based on poison 5 | """ 6 | @behaviour ExBankID.Json.Handler 7 | 8 | @spec decode(String.t(), struct()) :: {:ok, struct()} | {:error, String.t()} 9 | def decode(json, target) do 10 | Poison.decode(json, as: target) 11 | |> handle_decode_json() 12 | end 13 | 14 | @spec decode(String.t()) :: {:ok, struct()} | {:error, String.t()} 15 | def decode(json) do 16 | json 17 | |> Poison.decode() 18 | |> handle_decode_json() 19 | end 20 | 21 | defp handle_decode_json({:ok, struct}), do: {:ok, struct} 22 | defp handle_decode_json({:error, :invalid}), do: {:error, "invalid json"} 23 | defp handle_decode_json({:error, {:invalid, reason}}), do: {:error, reason} 24 | defp handle_decode_json({:error, {:invalid, reason, _pos}}), do: {:error, reason} 25 | 26 | @type payloads :: 27 | %ExBankID.Auth.Payload{} 28 | | %ExBankID.Sign.Payload{} 29 | | %ExBankID.Collect.Payload{} 30 | | %ExBankID.Cancel.Payload{} 31 | @spec encode!(payloads()) :: String.t() 32 | def encode!(payload) do 33 | Poison.encode!(payload) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/ex_bank_id/cancel/payload.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Cancel.Payload do 2 | @moduledoc """ 3 | Provides the struct used when requesting to cancel a pending authentication or signing 4 | """ 5 | defstruct [:orderRef] 6 | 7 | @doc """ 8 | Returns a Cancel Payload given a orderRef, Auth response struct or Sign response struct 9 | 10 | ## Examples 11 | iex> ExBankID.Cancel.Payload.new("131daac9-16c6-4618-beb0-365768f37288") 12 | %ExBankID.Cancel.Payload{orderRef: "131daac9-16c6-4618-beb0-365768f37288"} 13 | 14 | iex> ExBankID.Cancel.Payload.new("Not-a-valid-UUID") 15 | {:error, "OrderRef is not a valid UUID"} 16 | 17 | iex> %ExBankID.Auth.Response{orderRef: "131daac9-16c6-4618-beb0-365768f37288"} |> ExBankID.Cancel.Payload.new() 18 | %ExBankID.Cancel.Payload{orderRef: "131daac9-16c6-4618-beb0-365768f37288"} 19 | 20 | iex> %ExBankID.Sign.Response{orderRef: "131daac9-16c6-4618-beb0-365768f37288"} |> ExBankID.Cancel.Payload.new() 21 | %ExBankID.Cancel.Payload{orderRef: "131daac9-16c6-4618-beb0-365768f37288"} 22 | """ 23 | @spec new(binary | %ExBankID.Auth.Response{} | %ExBankID.Sign.Response{}) :: 24 | {:error, String.t()} | %__MODULE__{orderRef: String.t()} 25 | def new(%ExBankID.Auth.Response{orderRef: order_ref}) when is_binary(order_ref) do 26 | %ExBankID.Cancel.Payload{} 27 | |> set_order_ref(order_ref) 28 | end 29 | 30 | def new(%ExBankID.Sign.Response{orderRef: order_ref}) when is_binary(order_ref) do 31 | %ExBankID.Cancel.Payload{} 32 | |> set_order_ref(order_ref) 33 | end 34 | 35 | def new(order_ref) when is_binary(order_ref) do 36 | %ExBankID.Cancel.Payload{} 37 | |> set_order_ref(order_ref) 38 | end 39 | 40 | defp set_order_ref(payload, order_ref) do 41 | case UUID.info(order_ref) do 42 | {:ok, _} -> 43 | %{payload | orderRef: order_ref} 44 | 45 | _ -> 46 | {:error, "OrderRef is not a valid UUID"} 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/ex_bank_id/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Auth do 2 | def options() do 3 | [ 4 | url: [ 5 | type: :string, 6 | default: 7 | Application.get_env(:ex_bank_id, :url, "https://appapi2.test.bankid.com/rp/v5.1/") 8 | ], 9 | cert_file: [ 10 | type: :string, 11 | default: 12 | Application.get_env(:ex_bank_id, :cert_file, __DIR__ <> "/../../assets/test.pem"), 13 | doc: 14 | "If no certificate path is specified, the publicly available test certificate will be used." 15 | ], 16 | personal_number: [ 17 | type: :string, 18 | doc: 19 | "This option can be used to specify the personal number of the person authenticating. See: [BankID Relying party guidelines section 14.1](https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v3.4.pdf)" 20 | # TODO: Add validator 21 | ], 22 | requirement: [ 23 | doc: 24 | "See: [BankID Relying party guidelines section 14.5](https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v3.4.pdf)" 25 | ], 26 | http_client: [ 27 | type: :atom, 28 | default: Application.get_env(:ex_bank_id, :http_client, ExBankID.Http.Default), 29 | doc: 30 | "Specify a custom http client. Should be a module that implements ExBankID.Http.Client." 31 | ], 32 | json_handler: [ 33 | type: :atom, 34 | default: Application.get_env(:ex_bank_id, :json_handler, ExBankID.Json.Default), 35 | doc: 36 | "Specify a custom json handler. Should be a module that implements ExBankID.Json.Handler." 37 | ] 38 | ] 39 | end 40 | 41 | def auth(ip_address, opts) when is_binary(ip_address) and is_list(opts) do 42 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 43 | payload = %ExBankID.Auth.Payload{} <- ExBankID.Auth.Payload.new(ip_address, opts) do 44 | ExBankID.HttpRequest.send_request(payload, opts) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/ex_bank_id/cancel.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Cancel do 2 | alias ExBankID.Cancel.Payload 3 | 4 | def options() do 5 | [ 6 | url: [ 7 | type: :string, 8 | default: 9 | Application.get_env(:ex_bank_id, :url, "https://appapi2.test.bankid.com/rp/v5.1/") 10 | ], 11 | cert_file: [ 12 | type: :string, 13 | default: 14 | Application.get_env(:ex_bank_id, :cert_file, __DIR__ <> "/../../assets/test.pem"), 15 | doc: 16 | "If no certificate path is specified, the publicly available test certificate will be used." 17 | ], 18 | http_client: [ 19 | type: :atom, 20 | default: Application.get_env(:ex_bank_id, :http_client, ExBankID.Http.Default), 21 | doc: 22 | "Specify a custom http client. Should be a module that implements ExBankID.Http.Client." 23 | ], 24 | json_handler: [ 25 | type: :atom, 26 | default: Application.get_env(:ex_bank_id, :json_handler, ExBankID.Json.Default), 27 | doc: 28 | "Specify a custom json handler. Should be a module that implements ExBankID.Json.Handler." 29 | ] 30 | ] 31 | end 32 | 33 | def cancel(token, opts \\ []) 34 | 35 | def cancel(token, opts) when is_binary(token) do 36 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 37 | payload = %Payload{} <- 38 | Payload.new(token) do 39 | ExBankID.HttpRequest.send_request(payload, opts) 40 | end 41 | end 42 | 43 | def cancel(token = %ExBankID.Auth.Response{}, opts) do 44 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 45 | payload = %Payload{} <- 46 | Payload.new(token) do 47 | ExBankID.HttpRequest.send_request(payload, opts) 48 | end 49 | end 50 | 51 | def cancel(token = %ExBankID.Sign.Response{}, opts) do 52 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 53 | payload = %Payload{} <- 54 | Payload.new(token) do 55 | ExBankID.HttpRequest.send_request(payload, opts) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/ex_bank_id/collect.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Collect do 2 | alias ExBankID.Collect.Payload 3 | 4 | def options() do 5 | [ 6 | url: [ 7 | type: :string, 8 | default: 9 | Application.get_env(:ex_bank_id, :url, "https://appapi2.test.bankid.com/rp/v5.1/") 10 | ], 11 | cert_file: [ 12 | type: :string, 13 | default: 14 | Application.get_env(:ex_bank_id, :cert_file, __DIR__ <> "/../../assets/test.pem"), 15 | doc: 16 | "If no certificate path is specified, the publicly available test certificate will be used." 17 | ], 18 | http_client: [ 19 | type: :atom, 20 | default: Application.get_env(:ex_bank_id, :http_client, ExBankID.Http.Default), 21 | doc: 22 | "Specify a custom http client. Should be a module that implements ExBankID.Http.Client." 23 | ], 24 | json_handler: [ 25 | type: :atom, 26 | default: Application.get_env(:ex_bank_id, :json_handler, ExBankID.Json.Default), 27 | doc: 28 | "Specify a custom json handler. Should be a module that implements ExBankID.Json.Handler." 29 | ] 30 | ] 31 | end 32 | 33 | def collect(token, opts \\ []) 34 | 35 | def collect(token, opts) when is_binary(token) do 36 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 37 | payload = %Payload{} <- 38 | Payload.new(token) do 39 | ExBankID.HttpRequest.send_request(payload, opts) 40 | end 41 | end 42 | 43 | def collect(token = %ExBankID.Auth.Response{}, opts) do 44 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 45 | payload = %Payload{} <- 46 | Payload.new(token) do 47 | ExBankID.HttpRequest.send_request(payload, opts) 48 | end 49 | end 50 | 51 | def collect(token = %ExBankID.Sign.Response{}, opts) do 52 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 53 | payload = %Payload{} <- 54 | Payload.new(token) do 55 | ExBankID.HttpRequest.send_request(payload, opts) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /assets/test_cacert.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF0DCCA7igAwIBAgIIIhYaxu4khgAwDQYJKoZIhvcNAQENBQAwbDEkMCIGA1UE 3 | CgwbRmluYW5zaWVsbCBJRC1UZWtuaWsgQklEIEFCMRowGAYDVQQLDBFJbmZyYXN0 4 | cnVjdHVyZSBDQTEoMCYGA1UEAwwfVGVzdCBCYW5rSUQgU1NMIFJvb3QgQ0EgdjEg 5 | VGVzdDAeFw0xNDExMjExMjM5MzFaFw0zNDEyMzExMjM5MzFaMGwxJDAiBgNVBAoM 6 | G0ZpbmFuc2llbGwgSUQtVGVrbmlrIEJJRCBBQjEaMBgGA1UECwwRSW5mcmFzdHJ1 7 | Y3R1cmUgQ0ExKDAmBgNVBAMMH1Rlc3QgQmFua0lEIFNTTCBSb290IENBIHYxIFRl 8 | c3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAKWsJc/kV/0434d+S 9 | qn19mIr85RZ/PgRFaUplSrnhuzAmaXihPLCEsd3Mh/YErygcxhQ/MAzi5OZ/anfu 10 | WSCwceRlQINtvlRPdMoeZtu29FsntK1Z5r2SYNdFwbRFb8WN9FsU0KvC5zVnuDMg 11 | s5dUZwTmdzX5ZdLP7pdgB3zhTnra5ORtkiWiUxJVev9keRgAo00ZHIRJ+xTfiSPd 12 | Jc314maigVRQZdGKSyQcQMTWi1YLwd2zwOacNxleYf8xqKgkZsmkrc4Dp2mR5Pkr 13 | nnKB6A7sAOSNatua7M86EgcGi9AaEyaRMkYJImbBfzaNlaBPyMSvwmBZzp2xKc9O 14 | D3U06ogV6CJjJL7hSuVc5x/2H04d+2I+DKwep6YBoVL9L81gRYRycqg+w+cTZ1TF 15 | /s6NC5YRKSeOCrLw3ombhjyyuPl8T/h9cpXt6m3y2xIVLYVzeDhaql3hdi6IpRh6 16 | rwkMhJ/XmOpbDinXb1fWdFOyQwqsXQWOEwKBYIkM6cPnuid7qwaxfP22hDgAolGM 17 | LY7TPKUPRwV+a5Y3VPl7h0YSK7lDyckTJdtBqI6d4PWQLnHakUgRQy69nZhGRtUt 18 | PMSJ7I4Qtt3B6AwDq+SJTggwtJQHeid0jPki6pouenhPQ6dZT532x16XD+WIcD2f 19 | //XzzOueS29KB7lt/wH5K6EuxwIDAQABo3YwdDAdBgNVHQ4EFgQUDY6XJ/FIRFX3 20 | dB4Wep3RVM84RXowDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQNjpcn8UhE 21 | Vfd0HhZ6ndFUzzhFejARBgNVHSAECjAIMAYGBCoDBAUwDgYDVR0PAQH/BAQDAgEG 22 | MA0GCSqGSIb3DQEBDQUAA4ICAQA5s59/Olio4svHXiKu7sPQRvrf4GfGB7hUjBGk 23 | YW2YOHTYnHavSqlBASHc8gGGwuc7v7+H+vmOfSLZfGDqxnBqeJx1H5E0YqEXtNqW 24 | G1JusIFa9xWypcONjg9v7IMnxxQzLYws4YwgPychpMzWY6B5hZsjUyKgB+1igxnf 25 | uaBueLPw3ZaJhcCL8gz6SdCKmQpX4VaAadS0vdMrBOmd826H+aDGZek1vMjuH11F 26 | fJoXY2jyDnlol7Z4BfHc011toWNMxojI7w+U4KKCbSxpWFVYITZ8WlYHcj+b2A1+ 27 | dFQZFzQN+Y1Wx3VIUqSks6P7F5aF/l4RBngy08zkP7iLA/C7rm61xWxTmpj3p6SG 28 | fUBsrsBvBgfJQHD/Mx8U3iQCa0Vj1XPogE/PXQQq2vyWiAP662hD6og1/om3l1PJ 29 | TBUyYXxqJO75ux8IWblUwAjsmTlF/Pcj8QbcMPXLMTgNQAgarV6guchjivYqb6Zr 30 | hq+Nh3JrF0HYQuMgExQ6VX8T56saOEtmlp6LSQi4HvKatCNfWUJGoYeT5SrcJ6sn 31 | By7XLMhQUCOXcBwKbNvX6aP79VA3yeJHZO7XParX7V9BB+jtf4tz/usmAT/+qXtH 32 | CCv9Xf4lv8jgdOnFfXbXuT8I4gz8uq8ElBlpbJntO6p/NY5a08E6C7FWVR+WJ5vZ 33 | OP2HsA== 34 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.2.2" 5 | 6 | def project do 7 | [ 8 | app: :ex_bank_id, 9 | version: @version, 10 | elixir: "~> 1.10", 11 | start_permanent: Mix.env() == :prod, 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | test_coverage: [tool: ExCoveralls], 14 | preferred_cli_env: [ 15 | coveralls: :test, 16 | "coveralls.detail": :test, 17 | "coveralls.post": :test, 18 | "coveralls.html": :test 19 | ], 20 | deps: deps(), 21 | description: description(), 22 | package: package(), 23 | docs: docs() 24 | ] 25 | end 26 | 27 | # Run "mix help compile.app" to learn about applications. 28 | def application do 29 | [ 30 | extra_applications: [:logger] 31 | ] 32 | end 33 | 34 | # Run "mix help deps" to learn about dependencies. 35 | defp deps do 36 | [ 37 | {:httpoison, "~> 1.7", optional: true}, 38 | {:poison, "~> 4.0 or ~> 3.1", optional: true}, 39 | {:uuid, "~> 1.1"}, 40 | {:credo, "~> 1.4", only: [:dev], runtime: false}, 41 | {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, 42 | {:bypass, "~> 2.0", only: :test}, 43 | {:excoveralls, "~> 0.10", only: :test}, 44 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 45 | {:nimble_options, "~> 0.3.0"} 46 | ] 47 | end 48 | 49 | defp description(), do: "exBankID is a simple stateless API-client for the Swedish BankID API" 50 | 51 | defp package() do 52 | [ 53 | name: "exBankID", 54 | files: ~w(lib .formatter.exs mix.exs README* LICENSE* assets), 55 | licenses: ["MIT"], 56 | links: %{"GitHub" => "https://github.com/anfly0/exBankID"} 57 | ] 58 | end 59 | 60 | defp docs() do 61 | [ 62 | main: "readme", 63 | name: "ExBankID", 64 | source_ref: "v#{@version}", 65 | canonical: "http://hexdocs.pm/ExBankID", 66 | source_url: "https://github.com/anfly0/exBankID", 67 | extras: [ 68 | "README.md" 69 | ] 70 | ] 71 | end 72 | 73 | defp elixirc_paths(:test), do: ["lib", "test/helpers"] 74 | defp elixirc_paths(_), do: ["lib"] 75 | end 76 | -------------------------------------------------------------------------------- /lib/ex_bank_id.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID do 2 | @moduledoc """ 3 | Simple abstraction over the swedish BankID API. 4 | """ 5 | 6 | @doc """ 7 | Initiates a new BankID authentication session. 8 | 9 | Supported options:\n#{NimbleOptions.docs(ExBankID.Auth.options())} 10 | """ 11 | 12 | @spec auth(String.t(), Keyword.t()) :: 13 | {:error, %ExBankID.Error.Api{} | binary()} 14 | | {:error, NimbleOptions.ValidationError.t()} 15 | | {:ok, %ExBankID.Auth.Response{}} 16 | defdelegate auth(ip_address, opts \\ []), to: ExBankID.Auth 17 | 18 | @doc """ 19 | Initiates a new BankID signing session. 20 | 21 | Supported options:\n#{NimbleOptions.docs(ExBankID.Sign.options())} 22 | """ 23 | 24 | @spec sign(String.t(), String.t(), Keyword.t()) :: 25 | {:error, %ExBankID.Error.Api{} | binary()} 26 | | {:error, NimbleOptions.ValidationError.t()} 27 | | {:ok, %ExBankID.Sign.Response{}} 28 | defdelegate sign(ip_address, user_visible_data, opts \\ []), to: ExBankID.Sign 29 | 30 | @doc """ 31 | Attempts to collect the status of an ongoing authentication/signing session. See: [BankID Relying party guidelines section 14.2](https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v3.4.pdf) 32 | 33 | Supported options:\n#{NimbleOptions.docs(ExBankID.Collect.options())} 34 | """ 35 | @spec collect(String.t() | %ExBankID.Sign.Response{} | %ExBankID.Auth.Response{}, Keyword.t()) :: 36 | {:error, %ExBankID.Error.Api{} | binary()} 37 | | {:error, NimbleOptions.ValidationError.t()} 38 | | {:ok, %ExBankID.Collect.Response{}} 39 | defdelegate collect(order_ref, opts \\ []), to: ExBankID.Collect 40 | 41 | @doc """ 42 | Attempts to cancel a ongoing authentication/signing session. 43 | 44 | Supported options:\n#{NimbleOptions.docs(ExBankID.Cancel.options())} 45 | """ 46 | @spec cancel(String.t() | %ExBankID.Sign.Response{} | %ExBankID.Auth.Response{}, Keyword.t()) :: 47 | {:error, %ExBankID.Error.Api{} | binary()} 48 | | {:error, NimbleOptions.ValidationError.t()} 49 | | {:ok, %{}} 50 | defdelegate cancel(order_ref, opts \\ []), to: ExBankID.Cancel 51 | 52 | @spec static_qr(%ExBankID.Sign.Response{} | %ExBankID.Auth.Response{}) :: <<_::64, _::_*8>> 53 | defdelegate static_qr(response), to: ExBankID.Qr 54 | end 55 | -------------------------------------------------------------------------------- /lib/ex_bank_id/sign.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Sign do 2 | alias ExBankID.Sign 3 | 4 | def options() do 5 | [ 6 | url: [ 7 | type: :string, 8 | default: 9 | Application.get_env(:ex_bank_id, :url, "https://appapi2.test.bankid.com/rp/v5.1/") 10 | ], 11 | cert_file: [ 12 | type: :string, 13 | default: 14 | Application.get_env(:ex_bank_id, :cert_file, __DIR__ <> "/../../assets/test.pem"), 15 | doc: 16 | "If no certificate path is specified, the publicly available test certificate will be used." 17 | ], 18 | personal_number: [ 19 | type: :string, 20 | doc: 21 | "This option can be used to specify the personal number of the person signing. See: [BankID Relying party guidelines section 14.1](https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v3.4.pdf)" 22 | # TODO: Add validator 23 | ], 24 | requirement: [ 25 | doc: 26 | "See: [BankID Relying party guidelines section 14.5](https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v3.4.pdf)" 27 | ], 28 | user_non_visible_data: [ 29 | type: :string, 30 | doc: 31 | "Typically used to include a hash/digest of a document that is to be signed. See: [BankID Relying party guidelines section 12](https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v3.4.pdf)" 32 | ], 33 | http_client: [ 34 | type: :atom, 35 | default: Application.get_env(:ex_bank_id, :http_client, ExBankID.Http.Default), 36 | doc: 37 | "Specify a custom http client. Should be a module that implements ExBankID.Http.Client." 38 | ], 39 | json_handler: [ 40 | type: :atom, 41 | default: Application.get_env(:ex_bank_id, :json_handler, ExBankID.Json.Default), 42 | doc: 43 | "Specify a custom json handler. Should be a module that implements ExBankID.Json.Handler." 44 | ] 45 | ] 46 | end 47 | 48 | def sign(ip_address, user_visible_data, opts \\ []) 49 | when is_binary(ip_address) and is_binary(user_visible_data) and is_list(opts) do 50 | with {:ok, opts} <- NimbleOptions.validate(opts, options()), 51 | payload = %Sign.Payload{} <- Sign.Payload.new(ip_address, user_visible_data, opts) do 52 | ExBankID.HttpRequest.send_request(payload, opts) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/ex_bank_id/auth/payload.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Auth.Payload do 2 | @moduledoc """ 3 | Provides the struct used when initiating a authentication 4 | """ 5 | defstruct [:endUserIp, :personalNumber, :requirement] 6 | 7 | import ExBankID.PayloadHelpers 8 | 9 | @type reason :: binary() 10 | 11 | @spec new(binary, Keyword.t()) :: 12 | {:error, reason} 13 | | %__MODULE__{endUserIp: binary, personalNumber: binary(), requirement: map() | nil} 14 | @doc """ 15 | Returns a Payload struct containing the given ip address and personal number. 16 | 17 | ## Examples 18 | iex> ExBankID.Auth.Payload.new("1.1.1.1") 19 | %ExBankID.Auth.Payload{endUserIp: "1.1.1.1"} 20 | 21 | iex> ExBankID.Auth.Payload.new("qwerty") 22 | {:error, "Invalid ip address: qwerty"} 23 | 24 | iex> ExBankID.Auth.Payload.new("1.1.1.1", [personal_number: "190000000000"]) 25 | %ExBankID.Auth.Payload{endUserIp: "1.1.1.1", personalNumber: "190000000000"} 26 | 27 | iex> ExBankID.Auth.Payload.new("1.1.1.1", [personal_number: "Not a personal number"]) 28 | {:error, "Invalid personal number: Not a personal number"} 29 | 30 | iex> ExBankID.Auth.Payload.new("1.1.1.1", [requirement: %{allowFingerprint: :false}]) 31 | %ExBankID.Auth.Payload{endUserIp: "1.1.1.1", requirement: %{allowFingerprint: :false}} 32 | 33 | iex> ExBankID.Auth.Payload.new("1.1.1.1", [requirement: %{cardReader: "class2", tokenStartRequired: :false}]) 34 | %ExBankID.Auth.Payload{endUserIp: "1.1.1.1", requirement: %{cardReader: "class2", tokenStartRequired: :false}} 35 | 36 | iex> ExBankID.Auth.Payload.new("1.1.1.1", [requirement: %{issuerCn: ["Nordea CA for Smartcard users 12", "Nordea CA for Softcert users 13"] }]) 37 | %ExBankID.Auth.Payload{endUserIp: "1.1.1.1", requirement: %{issuerCn: ["Nordea CA for Smartcard users 12", "Nordea CA for Softcert users 13"]}} 38 | 39 | iex> ExBankID.Auth.Payload.new("1.1.1.1", [requirement: %{certificatePolicies: ["1.2.752.78.1.2", "1.2.752.78.*", "1.2.752.78.1.*"] }]) 40 | %ExBankID.Auth.Payload{endUserIp: "1.1.1.1", requirement: %{certificatePolicies: ["1.2.752.78.1.2", "1.2.752.78.*", "1.2.752.78.1.*"] }} 41 | 42 | iex> ExBankID.Auth.Payload.new("1.1.1.1", [requirement: %{notRealRequirement: ["shouldFail"]}]) 43 | {:error, "Invalid requirement"} 44 | """ 45 | def new(ip_address, opts \\ []) when is_binary(ip_address) and is_list(opts) do 46 | with {:ok, ip_address} <- check_ip_address(ip_address), 47 | {:ok, personal_number} <- check_personal_number(Keyword.get(opts, :personal_number)), 48 | {:ok, requirement} <- check_requirement(Keyword.get(opts, :requirement)) do 49 | %__MODULE__{ 50 | endUserIp: ip_address, 51 | personalNumber: personal_number, 52 | requirement: requirement 53 | } 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/cancel/client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Cancel.Client do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | bypass = Bypass.open() 6 | {:ok, bypass: bypass} 7 | end 8 | 9 | test "Client handles successful cancel request", %{bypass: bypass} do 10 | expected_response = {:ok, %{}} 11 | expected_request_payload = %{"orderRef" => "131daac9-16c6-4618-beb0-365768f37288"} 12 | response_payload = ~s<{}> 13 | 14 | Bypass.expect_once( 15 | bypass, 16 | "POST", 17 | "/cancel", 18 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 19 | ) 20 | 21 | assert ^expected_response = 22 | ExBankID.cancel("131daac9-16c6-4618-beb0-365768f37288", 23 | url: Test.Helpers.get_url(bypass.port()) 24 | ) 25 | end 26 | 27 | test "Client handles successful cancel request give a auth response struct", %{bypass: bypass} do 28 | expected_response = {:ok, %{}} 29 | expected_request_payload = %{"orderRef" => "131daac9-16c6-4618-beb0-365768f37288"} 30 | response_payload = ~s<{}> 31 | 32 | Bypass.expect_once( 33 | bypass, 34 | "POST", 35 | "/cancel", 36 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 37 | ) 38 | 39 | assert ^expected_response = 40 | ExBankID.cancel(%ExBankID.Auth.Response{orderRef: "131daac9-16c6-4618-beb0-365768f37288"}, 41 | url: Test.Helpers.get_url(bypass.port()) 42 | ) 43 | end 44 | 45 | test "Client handles successful cancel request give a sign response struct", %{bypass: bypass} do 46 | expected_response = {:ok, %{}} 47 | expected_request_payload = %{"orderRef" => "131daac9-16c6-4618-beb0-365768f37288"} 48 | response_payload = ~s<{}> 49 | 50 | Bypass.expect_once( 51 | bypass, 52 | "POST", 53 | "/cancel", 54 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 55 | ) 56 | 57 | assert ^expected_response = 58 | ExBankID.cancel(%ExBankID.Sign.Response{orderRef: "131daac9-16c6-4618-beb0-365768f37288"}, 59 | url: Test.Helpers.get_url(bypass.port()) 60 | ) 61 | end 62 | 63 | test "Client handles cancel request that results in API error", %{bypass: bypass} do 64 | expected_response = {:error, %ExBankID.Error.Api{errorCode: "invalidParameters", details: "No such order"}} 65 | expected_request_payload = %{"orderRef" => "131daac9-16c6-4618-beb0-365768f37288"} 66 | response_payload = ~s<{ 67 | "errorCode": "invalidParameters", 68 | "details": "No such order" 69 | }> 70 | 71 | Bypass.expect_once( 72 | bypass, 73 | "POST", 74 | "/cancel", 75 | Test.Helpers.endpoint_handler(400, response_payload, expected_request_payload) 76 | ) 77 | 78 | assert ^expected_response = 79 | ExBankID.cancel("131daac9-16c6-4618-beb0-365768f37288", 80 | url: Test.Helpers.get_url(bypass.port()) 81 | ) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/ex_bank_id/sign/payload.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.Sign.Payload do 2 | @moduledoc """ 3 | Provides the struct used when initiating a signing of data 4 | """ 5 | defstruct [:endUserIp, :personalNumber, :requirement, :userVisibleData, :userNonVisibleData] 6 | 7 | import ExBankID.PayloadHelpers 8 | 9 | @spec new( 10 | binary, 11 | binary, 12 | personal_number: String.t(), 13 | requirement: map(), 14 | user_non_visible_data: String.t() 15 | ) :: 16 | {:error, String.t()} | %ExBankID.Sign.Payload{} 17 | @doc """ 18 | Constructs a new Sign Payload with the given ip-address, user visible data, and optionally personal number and user non visible data. 19 | user_visible_data and user_non_visible_data will be properly encode 20 | 21 | ## Examples 22 | iex> ExBankID.Sign.Payload.new("1.1.1.1", "This will be visible in the bankID app") 23 | %ExBankID.Sign.Payload{endUserIp: "1.1.1.1", userVisibleData: "VGhpcyB3aWxsIGJlIHZpc2libGUgaW4gdGhlIGJhbmtJRCBhcHA="} 24 | 25 | iex> ExBankID.Sign.Payload.new("1.1.1.1", "This will be visible in the bankID app", personal_number: "190000000000") 26 | %ExBankID.Sign.Payload{endUserIp: "1.1.1.1", personalNumber: "190000000000", userVisibleData: "VGhpcyB3aWxsIGJlIHZpc2libGUgaW4gdGhlIGJhbmtJRCBhcHA="} 27 | 28 | iex> ExBankID.Sign.Payload.new("1.1.1.1", "This will be visible in the bankID app", requirement: %{allowFingerprint: :false}) 29 | %ExBankID.Sign.Payload{endUserIp: "1.1.1.1", userVisibleData: "VGhpcyB3aWxsIGJlIHZpc2libGUgaW4gdGhlIGJhbmtJRCBhcHA=", requirement: %{allowFingerprint: :false}} 30 | 31 | iex> ExBankID.Sign.Payload.new("Not a valid ip address", "This will be visible in the bankID app", personal_number: "190000000000") 32 | {:error, "Invalid ip address: Not a valid ip address"} 33 | """ 34 | def new(ip_address, user_visible_data, opts \\ []) 35 | when is_binary(ip_address) and is_binary(user_visible_data) and is_list(opts) do 36 | with {:ok, ip_address} <- check_ip_address(ip_address), 37 | {:ok, user_visible_data} <- encode_user_visible_data(user_visible_data), 38 | {:ok, personal_number} <- check_personal_number(Keyword.get(opts, :personal_number)), 39 | {:ok, user_non_visible_data} <- 40 | encode_user_non_visible_data(Keyword.get(opts, :user_non_visible_data)), 41 | {:ok, requirement} <- check_requirement(Keyword.get(opts, :requirement)) do 42 | %ExBankID.Sign.Payload{ 43 | endUserIp: ip_address, 44 | userVisibleData: user_visible_data, 45 | personalNumber: personal_number, 46 | requirement: requirement, 47 | userNonVisibleData: user_non_visible_data 48 | } 49 | end 50 | end 51 | 52 | defp encode_user_visible_data(data) when is_binary(data) do 53 | data = Base.encode64(data) 54 | 55 | if byte_size(data) < 40_000 do 56 | {:ok, data} 57 | else 58 | {:error, "User visible data is to large"} 59 | end 60 | end 61 | 62 | defp encode_user_non_visible_data(data) when is_binary(data) do 63 | data = Base.encode64(data) 64 | 65 | if byte_size(data) < 200_000 do 66 | {:ok, data} 67 | else 68 | {:error, "User visible data is to large"} 69 | end 70 | end 71 | 72 | defp encode_user_non_visible_data(nil) do 73 | {:ok, nil} 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/ex_bank_id/http_request.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.HttpRequest do 2 | @headers [{"Content-Type", "application/json"}] 3 | 4 | @type payload() :: 5 | %ExBankID.Auth.Payload{} 6 | | %ExBankID.Auth.Payload{} 7 | | %ExBankID.Cancel.Payload{} 8 | | %ExBankID.Collect.Payload{} 9 | | %ExBankID.Sign.Payload{} 10 | 11 | @type response() :: 12 | %ExBankID.Collect.Response{} 13 | | %ExBankID.Auth.Response{} 14 | | %ExBankID.Sign.Response{} 15 | | %{} 16 | 17 | @type opts() :: [url: String.t(), cert_file: String.t()] 18 | 19 | @spec send_request(payload(), opts()) :: {:error, %ExBankID.Error.Api{} | Binary} | {:ok, response()} 20 | def send_request(payload, opt \\ []) 21 | 22 | def send_request(payload = %ExBankID.Auth.Payload{}, opts) when is_list(opts) do 23 | do_send_request(:auth, payload, opts) 24 | end 25 | 26 | def send_request(payload = %ExBankID.Sign.Payload{}, opts) when is_list(opts) do 27 | do_send_request(:sign, payload, opts) 28 | end 29 | 30 | def send_request(payload = %ExBankID.Collect.Payload{}, opts) when is_list(opts) do 31 | do_send_request(:collect, payload, opts) 32 | end 33 | 34 | def send_request(payload = %ExBankID.Cancel.Payload{}, opts) when is_list(opts) do 35 | do_send_request(:cancel, payload, opts) 36 | end 37 | 38 | defp do_send_request(action, payload, opts) do 39 | client = Keyword.get(opts, :http_client) 40 | json_handler = Keyword.get(opts, :json_handler) 41 | 42 | client.post( 43 | url(action, opts), 44 | encode_payload(payload, json_handler), 45 | @headers, 46 | Keyword.get(opts, :cert_file) 47 | ) 48 | |> handle_response(action, json_handler) 49 | end 50 | 51 | defp url(:auth, opts), do: Keyword.get(opts, :url) <> "/auth" 52 | defp url(:sign, opts), do: Keyword.get(opts, :url) <> "/sign" 53 | defp url(:collect, opts), do: Keyword.get(opts, :url) <> "/collect" 54 | defp url(:cancel, opts), do: Keyword.get(opts, :url) <> "/cancel" 55 | 56 | defp handle_response({:ok, %ExBankID.Http.Response{status_code: 200, body: body}}, :collect, json_handler) do 57 | json_handler.decode(body, %ExBankID.Collect.Response{}) 58 | end 59 | 60 | defp handle_response({:ok, %ExBankID.Http.Response{status_code: 200, body: body}}, :auth, json_handler) do 61 | json_handler.decode(body, %ExBankID.Auth.Response{}) 62 | end 63 | 64 | defp handle_response({:ok, %ExBankID.Http.Response{status_code: 200, body: body}}, :sign, json_handler) do 65 | json_handler.decode(body, %ExBankID.Sign.Response{}) 66 | end 67 | 68 | defp handle_response({:ok, %ExBankID.Http.Response{status_code: 200, body: body}}, :cancel, json_handler) do 69 | json_handler.decode(body) 70 | end 71 | 72 | defp handle_response({:ok, %ExBankID.Http.Response{status_code: code, body: body}}, _, json_handler) do 73 | case json_handler.decode(body, %ExBankID.Error.Api{}) do 74 | {:ok, data} -> 75 | {:error, data} 76 | 77 | {:error, _} -> 78 | {:error, "Http code #{code}, could not decode body"} 79 | end 80 | end 81 | 82 | defp handle_response({:error, reason}, _, _) do 83 | {:error, reason} 84 | end 85 | 86 | defp encode_payload(payload, json_handler) do 87 | payload 88 | |> Map.from_struct() 89 | |> Enum.reject(fn {_key, value} -> is_nil(value) end) 90 | |> Map.new() 91 | |> json_handler.encode!() 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /assets/test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEyjCCArKgAwIBAgIILFi5Qu2eUu4wDQYJKoZIhvcNAQELBQAwcTELMAkGA1UE 3 | BhMCU0UxHTAbBgNVBAoMFFRlc3RiYW5rIEEgQUIgKHB1YmwpMRUwEwYDVQQFEwwx 4 | MTExMTExMTExMTExLDAqBgNVBAMMI1Rlc3RiYW5rIEEgUlAgQ0EgdjEgZm9yIEJh 5 | bmtJRCBUZXN0MB4XDTIwMDYxNzIyMDAwMFoXDTIyMDkwNTIxNTk1OVowcjELMAkG 6 | A1UEBhMCU0UxHTAbBgNVBAoMFFRlc3RiYW5rIEEgQUIgKHB1YmwpMRMwEQYDVQQF 7 | Ewo1NTY2MzA0OTI4MRcwFQYDVQQpDA5UZXN0IGF2IEJhbmtJRDEWMBQGA1UEAwwN 8 | RlAgVGVzdGNlcnQgMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCb 9 | Fluh4O4TEl4vydPGIUc4kAFDSVk1RM5TDYn8UDlWVxHVbalbXaJbtNQFYFm7lmpk 10 | FXiif50iupanvIq+k4DIGm01MnGasWl4EW9uoExCoZC4EemZry+Hk7hm2vbwGudf 11 | uIR8P43AD1MV7kp/skJaTH16qEeWTKQSoVlC+XNP/7Tl6Z8JE1GOR3+oAXWs+f/o 12 | 5SxXq4kIlBPkSK3tiTbEAP0/dNnSqSprv5MFHnTTWZyl8TK02TGrazyVUp/em6e6 13 | V/lTtJylBmHNJMpzl7PGixgXApRSMj4ltHwjqAizBMatDoXE6qXG0fEj+vhqSo/v 14 | wajY9t6FHNovhNdI+CcCAwEAAaNlMGMwEQYDVR0gBAowCDAGBgQqAwQFMA4GA1Ud 15 | DwEB/wQEAwIHgDAfBgNVHSMEGDAWgBTiuVUIvGKgRjldgAxQSpIBy0zvizAdBgNV 16 | HQ4EFgQU8xDQD1mLJ7MpUSxGB4lUDC5pdgswDQYJKoZIhvcNAQELBQADggIBAGWn 17 | PRoXUxPITv9Uo+4llmIHhHg5XR5ejenJOFyCvTAtteQozdFJ2rby+Q4WZNAdtP8Q 18 | tWcDaDigylDZSwi9TBGTRPSLH2cDFEWCQZVHs8svsF5VyBfkdtaRomiSAsk9KKLf 19 | 6Vo6ik1hlh4+NTBMX3VW0LjUZrPXmQ14El/XiJmHOvs54kAYf9ZTcO332Gqo8RF+ 20 | M3CRDVxPSrU34u6fvvxQuAvXvPumWvHaSAkOhpsn+Idr+KQ0Rip6fmgTG7UMicUi 21 | PxTE66xpaMsHDmuPaeC+cTK/iXAW60+X/Vv/ANn7UOz6tvrjo6Sd1DIpEEjqW/yE 22 | L4F05lbXhixKS2IRY+mAejoC66N2tz+0bv1grK4147jsYw4i9Y/rGyggkSrRd+1k 23 | QM7uBxW3Cu5fSKOUZ/0UTcBGf82Ze8SlbFFvpagELy9cJHwMKarzTkuX92hJ9KG0 24 | h26JBdOHzberG2tQiYzMPYVcch7WCAFWR++w6qInFs0WK7F7SBP0fyZew3hZZDoO 25 | snqLWMgG+YagjAsMAcr99RvwqX7TJtISejdxz9lxxN2jKM0b1f2v8K88tzRekrGG 26 | CPUQlnPu7sj7nPLVs5/sUEbaVRz8G8lKjYGsMuecRLpuVRQ/vPAd5whfiIzQFK76 27 | boWGbSHS6OXfIfDrowTNlzAP+/H9f7DyBZTdwrVX 28 | -----END CERTIFICATE----- 29 | Bag Attributes 30 | localKeyID: 93 12 D6 E8 2C AA 74 2E 52 10 29 3F 33 3C 39 7B 02 73 34 25 31 | Key Attributes: 32 | -----BEGIN PRIVATE KEY----- 33 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAmxZboeDuExJe 34 | L8nTxiFHOJABQ0lZNUTOUw2J/FA5VlcR1W2pW12iW7TUBWBZu5ZqZBV4on+dIrqW 35 | p7yKvpOAyBptNTJxmrFpeBFvbqBMQqGQuBHpma8vh5O4Ztr28BrnX7iEfD+NwA9T 36 | Fe5Kf7JCWkx9eqhHlkykEqFZQvlzT/+05emfCRNRjkd/qAF1rPn/6OUsV6uJCJQT 37 | 5Eit7Yk2xAD9P3TZ0qkqa7+TBR5001mcpfEytNkxq2s8lVKf3punulf5U7ScpQZh 38 | zSTKc5ezxosYFwKUUjI+JbR8I6gIswTGrQ6FxOqlxtHxI/r4akqP78Go2PbehRza 39 | L4TXSPgnAgMBAAECggEAdwrj5LrGxR7wiVpMCiI5S0XAa6dk3Eg6QLPAeHqEMwwU 40 | QKeDYdtgogrAVxMDnDJ/Iz68rpTw/vQKEzeVJsPncv86pijtBp4v7RoS3KapWLkO 41 | Ft5N4+3jAyNuv9iCmYGJf1wANZJ9zWTZk+bIIy+Nw8j/4cY/4A8bS4VgSEVG3GeQ 42 | /uUODFXSoSIi0mHfz5JTg+Tk739fSIjKbE5jvtFMIjp7Buj/clowKn2JN+yDCYPQ 43 | 7hkJ5TDm9+fwuyZ7/sBW/0QrYOEiAkNYIGFOGffxJJKR6TtP4eV5qrCtVPnqxNyB 44 | goEgZYbCh+3Ecslf6ZhSgLNreIy6OkTEarxJ6crwEQKBgQDZpu877IddIGqNRcxD 45 | UzrmUueQSwaD76MNkCl9CwO+O9YlAm6cXCmbeq+2lzfGI47XAPlPfLAwCDHU4qEH 46 | Eujunf47WmJEbh5BW8dPG/TOaMsPF+tgIEBIFKBg4+LFP9KYa49UZv4ak5SRm/lX 47 | bZIDh7Lat5aRS28AqaiOY7xK2wKBgQDiinSk0K3u7WV7RmfU7ZvvG0maOKVBykND 48 | xJS140UjC/oMjzxY9NIn4pZoR985g2sMC2NEwRX9RtuBk6lLTXqdDQBQT/ByWNZC 49 | waF7XCuE1+8YEbuChFEKyQYqvn6YsjRzy2giNRQ+5ShggskqpXNlbNdJiRDUfDOV 50 | Ht1Q1xj7pQKBgQCMxQZH+JQYLEYd9v3EsYkPvKEeVxfwr0YDGLFsuXoDSMoZB7io 51 | kocqkzAgZS9ijE7vSib1PQzrE/G+4ZEKdTWIV1E97BhQb/RLi2OeC9PKyEZFDdBj 52 | TJimxghwghOCReQcRrzd9vr0D21wu7OJ00kz1UldYo4UjPhPMmvdJC59LwKBgQDH 53 | Q54iMuQrW2l+K4m9Q1t70HbHTrgdzHmqLEnaS5ROpYRGc99TJ9WK+8Xs5/szraMF 54 | LyccHPLom+EMcwPglsAZUIxMGGSZUAb3JTaTOZmV+hH3C/Hxdc2LPRNNmc3lJir5 55 | B5wLKsEqKYuAiMnF105PkpMzvXquTKlaq5FkQC9beQKBgQC4zeRihfM5V49YRzuo 56 | bOj+5I1FVRgPHTqGMlNkzrzVk561kBSY6JfvPx4w5rylFTrBRM7IwwYNcnll0DVc 57 | 6cavNLs7O42rDEro5vAEpWiI6oMSGGtUsgzHTUENDxD36b/ZAGPshV8+J9cAdbgJ 58 | BRCv1g4xLPJrOU6D66R4VUSrGg== 59 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /test/sign/client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Auth.Sign do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | bypass = Bypass.open() 6 | {:ok, bypass: bypass} 7 | end 8 | 9 | test "client handles successful sign request without optional values", %{bypass: bypass} do 10 | Bypass.expect_once(bypass, "POST", "/sign", fn conn -> 11 | assert {:ok, body, _} = Plug.Conn.read_body(conn) 12 | 13 | assert {:ok, %{"userVisibleData" => "VmlzaWJsZSBkYXRh", "endUserIp" => "1.1.1.1"}} = Poison.decode(body) 14 | 15 | Plug.Conn.resp( 16 | conn, 17 | 200, 18 | ~s<{"orderRef":"131daac9-16c6-4618-beb0-365768f37288","autoStartToken":"7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6","qrStartToken":"67df3917-fa0d-44e5-b327-edcc928297f8","qrStartSecret":"d28db9a7-4cde-429e-a983-359be676944c"}> 19 | ) 20 | end) 21 | 22 | assert {:ok, 23 | %ExBankID.Sign.Response{ 24 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 25 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 26 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 27 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 28 | }} = ExBankID.sign("1.1.1.1", "Visible data", url: Test.Helpers.get_url(bypass.port())) 29 | end 30 | 31 | test "client handles successful sign request with personal number", %{bypass: bypass} do 32 | Bypass.expect_once(bypass, "POST", "/sign", fn conn -> 33 | Plug.Conn.resp( 34 | conn, 35 | 200, 36 | ~s<{"orderRef":"131daac9-16c6-4618-beb0-365768f37288","autoStartToken":"7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6","qrStartToken":"67df3917-fa0d-44e5-b327-edcc928297f8","qrStartSecret":"d28db9a7-4cde-429e-a983-359be676944c"}> 37 | ) 38 | end) 39 | 40 | assert {:ok, 41 | %ExBankID.Sign.Response{ 42 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 43 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 44 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 45 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 46 | }} = 47 | ExBankID.sign("1.1.1.1", "Visible data", 48 | url: Test.Helpers.get_url(bypass.port()), 49 | personal_number: "190000000000" 50 | ) 51 | end 52 | 53 | test "client handles successful sign request with request", %{bypass: bypass} do 54 | Bypass.expect_once(bypass, "POST", "/sign", fn conn -> 55 | Plug.Conn.resp( 56 | conn, 57 | 200, 58 | ~s<{"orderRef":"131daac9-16c6-4618-beb0-365768f37288","autoStartToken":"7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6","qrStartToken":"67df3917-fa0d-44e5-b327-edcc928297f8","qrStartSecret":"d28db9a7-4cde-429e-a983-359be676944c"}> 59 | ) 60 | end) 61 | 62 | assert {:ok, 63 | %ExBankID.Sign.Response{ 64 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 65 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 66 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 67 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 68 | }} = 69 | ExBankID.sign("1.1.1.1", "Visible data", 70 | url: Test.Helpers.get_url(bypass.port()), 71 | requirement: %{issuerCn: ["test"]} 72 | ) 73 | end 74 | 75 | test "client handles successful sign request with personal number and non visible data", %{ 76 | bypass: bypass 77 | } do 78 | Bypass.expect_once(bypass, "POST", "/sign", fn conn -> 79 | Plug.Conn.resp( 80 | conn, 81 | 200, 82 | ~s<{"orderRef":"131daac9-16c6-4618-beb0-365768f37288","autoStartToken":"7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6","qrStartToken":"67df3917-fa0d-44e5-b327-edcc928297f8","qrStartSecret":"d28db9a7-4cde-429e-a983-359be676944c"}> 83 | ) 84 | end) 85 | 86 | assert {:ok, 87 | %ExBankID.Sign.Response{ 88 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 89 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 90 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 91 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 92 | }} = 93 | ExBankID.sign("1.1.1.1", "Visible data", 94 | url: Test.Helpers.get_url(bypass.port()), 95 | personal_number: "190000000000", 96 | user_non_visible_data: "Not visible data" 97 | ) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/auth/client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Auth.Client do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | bypass = Bypass.open() 6 | {:ok, bypass: bypass} 7 | end 8 | 9 | test "client handles successful auth request", %{bypass: bypass} do 10 | expected_response = 11 | {:ok, 12 | %ExBankID.Auth.Response{ 13 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 14 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 15 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 16 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 17 | }} 18 | 19 | expected_request_payload = %{"endUserIp" => "1.1.1.1"} 20 | 21 | response_payload = ~s<{ 22 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 23 | "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 24 | "qrStartToken": "67df3917-fa0d-44e5-b327-edcc928297f8", 25 | "qrStartSecret": "d28db9a7-4cde-429e-a983-359be676944c" 26 | }> 27 | 28 | Bypass.expect_once( 29 | bypass, 30 | "POST", 31 | "/auth", 32 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 33 | ) 34 | 35 | assert ^expected_response = ExBankID.auth("1.1.1.1", url: Test.Helpers.get_url(bypass.port())) 36 | end 37 | 38 | test "client handles successful auth request with personal number", %{bypass: bypass} do 39 | expected_response = 40 | {:ok, 41 | %ExBankID.Auth.Response{ 42 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 43 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 44 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 45 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 46 | }} 47 | 48 | expected_request_payload = %{"endUserIp" => "1.1.1.1", "personalNumber" => "190000000000"} 49 | 50 | response_payload = ~s<{ 51 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 52 | "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 53 | "qrStartToken": "67df3917-fa0d-44e5-b327-edcc928297f8", 54 | "qrStartSecret": "d28db9a7-4cde-429e-a983-359be676944c" 55 | }> 56 | 57 | Bypass.expect_once( 58 | bypass, 59 | "POST", 60 | "/auth", 61 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 62 | ) 63 | 64 | assert ^expected_response = 65 | ExBankID.auth("1.1.1.1", 66 | url: Test.Helpers.get_url(bypass.port()), 67 | personal_number: "190000000000" 68 | ) 69 | end 70 | 71 | test "client handles successful auth request with requirement", %{bypass: bypass} do 72 | expected_response = 73 | {:ok, 74 | %ExBankID.Auth.Response{ 75 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 76 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 77 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 78 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 79 | }} 80 | 81 | expected_request_payload = %{ 82 | "endUserIp" => "1.1.1.1", 83 | "requirement" => %{"allowFingerprint" => true} 84 | } 85 | 86 | response_payload = ~s<{ 87 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 88 | "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 89 | "qrStartToken": "67df3917-fa0d-44e5-b327-edcc928297f8", 90 | "qrStartSecret": "d28db9a7-4cde-429e-a983-359be676944c" 91 | }> 92 | 93 | Bypass.expect_once( 94 | bypass, 95 | "POST", 96 | "/auth", 97 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 98 | ) 99 | 100 | assert ^expected_response = 101 | ExBankID.auth("1.1.1.1", 102 | url: Test.Helpers.get_url(bypass.port()), 103 | requirement: %{allowFingerprint: true} 104 | ) 105 | end 106 | 107 | test "client handles unsuccessful auth request", %{bypass: bypass} do 108 | expected_response = 109 | {:error, %ExBankID.Error.Api{errorCode: "invalidParameters", details: "No such order"}} 110 | 111 | expected_request_payload = %{"endUserIp" => "1.1.1.1"} 112 | 113 | response_payload = ~s<{ 114 | "errorCode": "invalidParameters", 115 | "details": "No such order" 116 | }> 117 | 118 | Bypass.expect_once( 119 | bypass, 120 | "POST", 121 | "/auth", 122 | Test.Helpers.endpoint_handler(400, response_payload, expected_request_payload) 123 | ) 124 | 125 | assert ^expected_response = ExBankID.auth("1.1.1.1", url: Test.Helpers.get_url(bypass.port())) 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/collect/client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Auth.Collect do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | bypass = Bypass.open() 6 | {:ok, bypass: bypass} 7 | end 8 | 9 | test "Client can collect pending order", %{bypass: bypass} do 10 | expected_response = 11 | {:ok, 12 | %ExBankID.Collect.Response{ 13 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 14 | status: "Pending", 15 | hintCode: "userSign" 16 | }} 17 | 18 | expected_request_payload = %{"orderRef" => "131daac9-16c6-4618-beb0-365768f37288"} 19 | 20 | response_payload = ~s<{ 21 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 22 | "status": "Pending", 23 | "hintCode": "userSign" 24 | }> 25 | 26 | Bypass.expect_once( 27 | bypass, 28 | "POST", 29 | "/collect", 30 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 31 | ) 32 | 33 | assert ^expected_response = 34 | ExBankID.collect("131daac9-16c6-4618-beb0-365768f37288", 35 | url: Test.Helpers.get_url(bypass.port()) 36 | ) 37 | end 38 | 39 | test "Client can collect pending order auth response as argument", %{bypass: bypass} do 40 | expected_response = 41 | {:ok, 42 | %ExBankID.Collect.Response{ 43 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 44 | status: "pending", 45 | hintCode: "userSign" 46 | }} 47 | 48 | expected_request_payload = %{"orderRef" => "131daac9-16c6-4618-beb0-365768f37288"} 49 | 50 | response_payload = ~s<{"orderRef":"131daac9-16c6-4618-beb0-365768f37288","status":"pending","hintCode":"userSign"}> 51 | 52 | Bypass.expect_once( 53 | bypass, 54 | "POST", 55 | "/collect", 56 | Test.Helpers.endpoint_handler(200, response_payload, expected_request_payload) 57 | ) 58 | 59 | assert ^expected_response = 60 | ExBankID.collect( 61 | %ExBankID.Auth.Response{ 62 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 63 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 64 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 65 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 66 | }, 67 | url: Test.Helpers.get_url(bypass.port()) 68 | ) 69 | end 70 | 71 | # TODO: Refactor this test to have the same structure as the other ones. 72 | test "Client can collect pending order sign response as argument", %{bypass: bypass} do 73 | Bypass.expect_once(bypass, "POST", "/collect", fn conn -> 74 | Plug.Conn.resp( 75 | conn, 76 | 200, 77 | ~s<{"orderRef":"131daac9-16c6-4618-beb0-365768f37288","status":"pending","hintCode":"userSign"}> 78 | ) 79 | end) 80 | 81 | assert {:ok, 82 | %ExBankID.Collect.Response{ 83 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 84 | status: "pending", 85 | hintCode: "userSign" 86 | }} = 87 | ExBankID.collect( 88 | %ExBankID.Sign.Response{ 89 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 90 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 91 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 92 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 93 | }, 94 | url: Test.Helpers.get_url(bypass.port()) 95 | ) 96 | end 97 | 98 | # TODO: Refactor this test to have the same structure as the other ones. 99 | test "Client can collect completed order", %{bypass: bypass} do 100 | Bypass.expect_once(bypass, "POST", "/collect", fn conn -> 101 | Plug.Conn.resp( 102 | conn, 103 | 200, 104 | ~s<{ 105 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 106 | "status": "complete", 107 | "completionData": { 108 | "user": { 109 | "personalNumber": "190000000000", 110 | "name": "Karl Karlsson", 111 | "givenName": "Karl", 112 | "surname": "Karlsson" 113 | }, 114 | "device": { 115 | "ipAddress": "192.168.0.1" 116 | }, 117 | "cert": { 118 | "notBefore": "1502983274000", 119 | "notAfter": "1563549674000" 120 | }, 121 | "signature": "base64-encoded data", 122 | "ocspResponse": "base64-encoded data" 123 | } 124 | }> 125 | ) 126 | end) 127 | 128 | assert {:ok, %ExBankID.Collect.Response{}} = 129 | ExBankID.collect( 130 | %ExBankID.Sign.Response{ 131 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 132 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 133 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 134 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 135 | }, 136 | url: Test.Helpers.get_url(bypass.port()) 137 | ) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exBankID 2 | ![license: MIT](https://img.shields.io/github/license/anfly0/exBankID) 3 | ![test](https://img.shields.io/github/workflow/status/anfly0/exbankid/Elixir%20CI/master) 4 | [![Coverage Status](https://coveralls.io/repos/github/anfly0/exBankID/badge.svg?branch=master)](https://coveralls.io/github/anfly0/exBankID?branch=master) 5 | ![hex version](https://img.shields.io/hexpm/v/exBankID) 6 | ## Introduction 7 | ExBankID is a simple stateless elixir client for the [Swedish BankID API](https://www.bankid.com/). 8 | 9 | ## Installation 10 | This library is available as a package on [hex.pm](https://hex.pm/packages/exBankID) and can be installed by 11 | adding ```{:ex_bank_id, "~> 0.2.2", hex: :exBankID}``` to your list of dependencies in ```mix.exs```. 12 | ### Optional dependencies: 13 | This library depends on an implementation of ```ExBankID.Http.Client``` and ```ExBankID.Json.Handler``` to be available. If no custom implementations are declared in the config or in the opts passed to the functions in ```ExBankID``` the default implementations will be used. 14 | __For the defaults to work__ the add this to your list of dependencies in ```mix.exs``` 15 | ```elixir 16 | {:poison, "~> 4.0"} # Add this to your deps if you want to use the default json handler 17 | {:httpoison, "~> 1.7"} # Add this to your deps if you want to use the default http client 18 | ``` 19 | 20 | 21 | ## Configuration 22 | ```elixir 23 | # config/config.exs 24 | 25 | config :ex_bank_id, 26 | # Using a custom http client. Should be a module that implements ExBankID.Http.Client. 27 | # Defaults to ExBankID.Http.Default 28 | http_client: MyApp.Http.Client 29 | 30 | # Using a custom json handler. Should be a module that implements ExBankID.Json.Handler. 31 | # Defaults to ExBankID.Json.Default 32 | json_handler: MyApp.Json.Handler 33 | 34 | # The path to the client cert file used to authenticate with the BankID API 35 | # Defaults to the test cert in the assets directory. 36 | cert_file: "/path/to/cert/file.pem" 37 | 38 | # BankID API url 39 | # Defaults to "https://appapi2.test.bankid.com/rp/v5.1/" 40 | url: "https://appapi2.bankid.com/rp//" 41 | 42 | ``` 43 | All the above configuration options can be overridden by setting the new value for the corresponding key in the opts Keyword list passed to any of the functions in ExBankID. 44 | 45 | __Example:__ 46 | ```elixir 47 | # This will override the url in the config. 48 | ExBankID.auth("1.1.1.1", url: "my.mock-server.local") 49 | 50 | # This will override the configured/default json handler and url 51 | ExBankID.auth("1.1.1.1", json_handler: Custom.Json.Handler, url: "my.mock-server.local") 52 | ``` 53 | 54 | 55 | ## Basic usage 56 | 57 | ```elixir 58 | 59 | # Authenticate with ip address and optionally the personal number (12 digits) 60 | iex> {:ok, authentication} = ExBankID.auth("1.1.1.1", personal_number: "190000000000") 61 | {:ok, 62 | %ExBankID.Auth.Response{ 63 | autoStartToken: "3241031e-d849-4e3a-a662-1a36e65eff93", 64 | orderRef: "9b69419c-b3ac-4f7c-9796-bf54f1a4e40b", 65 | qrStartSecret: "c0846df5-f96d-49c0-9ef5-4126cd9376e9", 66 | qrStartToken: "3fb97679-98cb-42da-afe6-62aecbaaab7e" 67 | }} 68 | 69 | # Collect the status of the initiated authentication either with the orderRef 70 | # or with the ExBankID.Auth.Response struct 71 | iex> {:ok, collect_response} = ExBankID.collect("9b69419c-b3ac-4f7c-9796-bf54f1a4e40b") 72 | {:ok, 73 | %ExBankID.Collect.Response{ 74 | completionData: %ExBankID.Collect.CompletionData{ 75 | cert: %{}, 76 | device: %{}, 77 | ocspResponse: nil, 78 | signature: nil, 79 | user: %ExBankID.Collect.User{ 80 | givenName: nil, 81 | name: nil, 82 | personalNumber: nil, 83 | surname: nil 84 | } 85 | }, 86 | hintCode: "outstandingTransaction", 87 | orderRef: "1fadf49f-c695-4bb3-869a-61aee9678009", 88 | status: "pending" 89 | }} 90 | 91 | # Using ExBankID.Auth.Response struct 92 | iex> {:ok, collect_response} = ExBankID.collect(authentication) 93 | {:ok, 94 | %ExBankID.Collect.Response{ 95 | completionData: %ExBankID.Collect.CompletionData{ 96 | cert: %{}, 97 | device: %{}, 98 | ocspResponse: nil, 99 | signature: nil, 100 | user: %ExBankID.Collect.User{ 101 | givenName: nil, 102 | name: nil, 103 | personalNumber: nil, 104 | surname: nil 105 | } 106 | }, 107 | hintCode: "outstandingTransaction", 108 | orderRef: "1fadf49f-c695-4bb3-869a-61aee9678009", 109 | status: "pending" 110 | }} 111 | 112 | # When authentication is completed by the end user the fields in CompletionData will 113 | # be populated. 114 | 115 | #User signing a given message. 116 | iex> {:ok, sign} = ExBankID.sign( 117 | "1.1.1.1", 118 | "This will be displayed in the BankID app", 119 | personal_number: "190000000000", # Optional 120 | user_non_visible_data: "Not displayed" # Optional 121 | ) 122 | {:ok, 123 | %ExBankID.Sign.Response{ 124 | autoStartToken: "c7b67410-c376-4d27-9aff-f7e331082619", 125 | orderRef: "90b3816d-c1d3-4650-aa4d-26d9996160de", 126 | qrStartSecret: "f28787ec-a554-4db4-90c6-dd662dd249bc", 127 | qrStartToken: "c7a2373b-9a7a-470f-816f-0af0c3d82053" 128 | }} 129 | # Collecting is done the same way as for a authentication. 130 | 131 | # Canceling a sign or authentication 132 | iex> {:ok, _} = ExBankID.cancel(authentication) 133 | {:ok, %{}} 134 | 135 | 136 | 137 | ``` -------------------------------------------------------------------------------- /test/json/json_default_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Json.Default do 2 | use ExUnit.Case, async: true 3 | 4 | test "Decoding auth response" do 5 | json = ~s<{ 6 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 7 | "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 8 | "qrStartToken": "67df3917-fa0d-44e5-b327-edcc928297f8", 9 | "qrStartSecret": "d28db9a7-4cde-429e-a983-359be676944c" 10 | }> 11 | 12 | assert {:ok, 13 | %ExBankID.Auth.Response{ 14 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 15 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 16 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 17 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 18 | }} = ExBankID.Json.Default.decode(json, %ExBankID.Auth.Response{}) 19 | end 20 | 21 | test "Decoding auth response api v5" do 22 | json = ~s<{ 23 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 24 | "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6" 25 | }> 26 | 27 | assert {:ok, 28 | %ExBankID.Auth.Response{ 29 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 30 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 31 | qrStartToken: nil, 32 | qrStartSecret: nil 33 | }} = ExBankID.Json.Default.decode(json, %ExBankID.Auth.Response{}) 34 | end 35 | 36 | test "Decoding sign response" do 37 | json = ~s<{ 38 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 39 | "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 40 | "qrStartToken": "67df3917-fa0d-44e5-b327-edcc928297f8", 41 | "qrStartSecret": "d28db9a7-4cde-429e-a983-359be676944c" 42 | }> 43 | 44 | assert {:ok, 45 | %ExBankID.Sign.Response{ 46 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 47 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 48 | qrStartToken: "67df3917-fa0d-44e5-b327-edcc928297f8", 49 | qrStartSecret: "d28db9a7-4cde-429e-a983-359be676944c" 50 | }} = ExBankID.Json.Default.decode(json, %ExBankID.Sign.Response{}) 51 | end 52 | 53 | test "Decoding sign response api v5" do 54 | json = ~s<{ 55 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 56 | "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6" 57 | }> 58 | 59 | assert {:ok, 60 | %ExBankID.Sign.Response{ 61 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 62 | autoStartToken: "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", 63 | qrStartToken: nil, 64 | qrStartSecret: nil 65 | }} = ExBankID.Json.Default.decode(json, %ExBankID.Sign.Response{}) 66 | end 67 | 68 | test "Decoding collect response" do 69 | json = ~s<{ 70 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 71 | "status": "Pending", 72 | "hintCode": "userSign" 73 | }> 74 | 75 | assert {:ok, 76 | %ExBankID.Collect.Response{ 77 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 78 | status: "Pending", 79 | hintCode: "userSign" 80 | }} = ExBankID.Json.Default.decode(json, %ExBankID.Collect.Response{}) 81 | end 82 | 83 | test "Decoding collect response with completion data" do 84 | json = ~s<{ 85 | "orderRef": "131daac9-16c6-4618-beb0-365768f37288", 86 | "status": "complete", 87 | "completionData": { 88 | "user": { 89 | "personalNumber": "190000000000", 90 | "name": "Karl Karlsson", 91 | "givenName": "Karl", 92 | "surname": "Karlsson" 93 | }, 94 | "device": { 95 | "ipAddress": "192.168.0.1" 96 | }, 97 | "cert": { 98 | "notBefore": "1502983274000", 99 | "notAfter": "1563549674000" 100 | }, 101 | "signature": "base64-encoded data", 102 | "ocspResponse": "base64-encoded data" 103 | } 104 | }> 105 | 106 | assert {:ok, 107 | %ExBankID.Collect.Response{ 108 | orderRef: "131daac9-16c6-4618-beb0-365768f37288", 109 | status: "complete", 110 | completionData: %ExBankID.Collect.CompletionData{ 111 | user: %ExBankID.Collect.User{ 112 | personalNumber: "190000000000", 113 | name: "Karl Karlsson", 114 | givenName: "Karl", 115 | surname: "Karlsson" 116 | }, 117 | device: %{ 118 | "ipAddress" => "192.168.0.1" 119 | }, 120 | cert: %{ 121 | "notBefore" => "1502983274000", 122 | "notAfter" => "1563549674000" 123 | }, 124 | signature: "base64-encoded data", 125 | ocspResponse: "base64-encoded data" 126 | } 127 | }} = ExBankID.Json.Default.decode(json, %ExBankID.Collect.Response{}) 128 | end 129 | 130 | test "Decode cancel response" do 131 | json = ~s<{}> 132 | 133 | assert {:ok, %{}} = ExBankID.Json.Default.decode(json) 134 | end 135 | 136 | test "Encode Auth payload" do 137 | payload = ExBankID.Auth.Payload.new("1.1.1.1", personal_number: "190000000000") 138 | 139 | assert ^payload = Poison.decode!(ExBankID.Json.Default.encode!(payload), as: %ExBankID.Auth.Payload{}) 140 | end 141 | 142 | test "Encode Sign payload" do 143 | payload = ExBankID.Sign.Payload.new("1.1.1.1", "some data to sign", personal_number: "190000000000") 144 | 145 | assert ^payload = Poison.decode!(ExBankID.Json.Default.encode!(payload), as: %ExBankID.Sign.Payload{}) 146 | end 147 | 148 | test "Encode Collect payload" do 149 | payload = ExBankID.Collect.Payload.new("131daac9-16c6-4618-beb0-365768f37288") 150 | 151 | assert ^payload = Poison.decode!(ExBankID.Json.Default.encode!(payload), as: %ExBankID.Collect.Payload{}) 152 | end 153 | 154 | test "Encode Cancel payload" do 155 | payload = ExBankID.Cancel.Payload.new("131daac9-16c6-4618-beb0-365768f37288") 156 | 157 | assert ^payload = Poison.decode!(ExBankID.Json.Default.encode!(payload), as: %ExBankID.Cancel.Payload{}) 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "test/" 27 | ], 28 | excluded: [~r"/_build/", ~r"/deps/"] 29 | }, 30 | # 31 | # Load and configure plugins here: 32 | # 33 | plugins: [], 34 | # 35 | # If you create your own checks, you must specify the source files for 36 | # them here, so they can be loaded by Credo before running the analysis. 37 | # 38 | requires: [], 39 | # 40 | # If you want to enforce a style guide and need a more traditional linting 41 | # experience, you can change `strict` to `true` below: 42 | # 43 | strict: true, 44 | # 45 | # To modify the timeout for parsing files, change this value: 46 | # 47 | parse_timeout: 5000, 48 | # 49 | # If you want to use uncolored output by default, you can change `color` 50 | # to `false` below: 51 | # 52 | color: true, 53 | # 54 | # You can customize the parameters of any check by adding a second element 55 | # to the tuple. 56 | # 57 | # To disable a check put `false` as second element: 58 | # 59 | # {Credo.Check.Design.DuplicatedCode, false} 60 | # 61 | checks: [ 62 | # 63 | ## Consistency Checks 64 | # 65 | {Credo.Check.Consistency.ExceptionNames, []}, 66 | {Credo.Check.Consistency.LineEndings, []}, 67 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 68 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 69 | {Credo.Check.Consistency.SpaceInParentheses, []}, 70 | {Credo.Check.Consistency.TabsOrSpaces, []}, 71 | 72 | # 73 | ## Design Checks 74 | # 75 | # You can customize the priority of any check 76 | # Priority values are: `low, normal, high, higher` 77 | # 78 | {Credo.Check.Design.AliasUsage, 79 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 80 | # You can also customize the exit_status of each check. 81 | # If you don't want TODO comments to cause `mix credo` to fail, just 82 | # set this value to 0 (zero). 83 | # 84 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 85 | {Credo.Check.Design.TagFIXME, []}, 86 | 87 | # 88 | ## Readability Checks 89 | # 90 | {Credo.Check.Readability.AliasOrder, []}, 91 | {Credo.Check.Readability.FunctionNames, []}, 92 | {Credo.Check.Readability.LargeNumbers, []}, 93 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 94 | {Credo.Check.Readability.ModuleAttributeNames, []}, 95 | {Credo.Check.Readability.ModuleDoc, []}, 96 | {Credo.Check.Readability.ModuleNames, []}, 97 | {Credo.Check.Readability.ParenthesesInCondition, []}, 98 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 99 | {Credo.Check.Readability.PredicateFunctionNames, []}, 100 | {Credo.Check.Readability.PreferImplicitTry, []}, 101 | {Credo.Check.Readability.RedundantBlankLines, []}, 102 | {Credo.Check.Readability.Semicolons, []}, 103 | {Credo.Check.Readability.SpaceAfterCommas, []}, 104 | {Credo.Check.Readability.StringSigils, []}, 105 | {Credo.Check.Readability.TrailingBlankLine, []}, 106 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 107 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 108 | {Credo.Check.Readability.VariableNames, []}, 109 | 110 | # 111 | ## Refactoring Opportunities 112 | # 113 | {Credo.Check.Refactor.CondStatements, []}, 114 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 115 | {Credo.Check.Refactor.FunctionArity, []}, 116 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 117 | {Credo.Check.Refactor.MapInto, []}, 118 | {Credo.Check.Refactor.MatchInCondition, []}, 119 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 120 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 121 | {Credo.Check.Refactor.Nesting, []}, 122 | {Credo.Check.Refactor.UnlessWithElse, []}, 123 | {Credo.Check.Refactor.WithClauses, []}, 124 | 125 | # 126 | ## Warnings 127 | # 128 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 129 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 130 | {Credo.Check.Warning.IExPry, []}, 131 | {Credo.Check.Warning.IoInspect, []}, 132 | {Credo.Check.Warning.LazyLogging, []}, 133 | {Credo.Check.Warning.MixEnv, false}, 134 | {Credo.Check.Warning.OperationOnSameValues, []}, 135 | {Credo.Check.Warning.OperationWithConstantResult, []}, 136 | {Credo.Check.Warning.RaiseInsideRescue, []}, 137 | {Credo.Check.Warning.UnusedEnumOperation, []}, 138 | {Credo.Check.Warning.UnusedFileOperation, []}, 139 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 140 | {Credo.Check.Warning.UnusedListOperation, []}, 141 | {Credo.Check.Warning.UnusedPathOperation, []}, 142 | {Credo.Check.Warning.UnusedRegexOperation, []}, 143 | {Credo.Check.Warning.UnusedStringOperation, []}, 144 | {Credo.Check.Warning.UnusedTupleOperation, []}, 145 | {Credo.Check.Warning.UnsafeExec, []}, 146 | 147 | # 148 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 149 | 150 | # 151 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 152 | # 153 | {Credo.Check.Readability.StrictModuleLayout, false}, 154 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 155 | {Credo.Check.Consistency.UnusedVariableNames, false}, 156 | {Credo.Check.Design.DuplicatedCode, []}, 157 | {Credo.Check.Readability.AliasAs, false}, 158 | {Credo.Check.Readability.MultiAlias, false}, 159 | {Credo.Check.Readability.Specs, []}, 160 | {Credo.Check.Readability.SinglePipe, false}, 161 | {Credo.Check.Readability.WithCustomTaggedTuple, false}, 162 | {Credo.Check.Refactor.ABCSize, false}, 163 | {Credo.Check.Refactor.AppendSingleItem, false}, 164 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 165 | {Credo.Check.Refactor.ModuleDependencies, []}, 166 | {Credo.Check.Refactor.NegatedIsNil, false}, 167 | {Credo.Check.Refactor.PipeChainStart, false}, 168 | {Credo.Check.Refactor.VariableRebinding, false}, 169 | {Credo.Check.Warning.LeakyEnvironment, false}, 170 | {Credo.Check.Warning.MapGetUnsafePass, false}, 171 | {Credo.Check.Warning.UnsafeToAtom, []} 172 | 173 | # 174 | # Custom checks can be created using `mix credo.gen.check`. 175 | # 176 | ] 177 | } 178 | ] 179 | } 180 | -------------------------------------------------------------------------------- /lib/ex_bank_id/payload_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule ExBankID.PayloadHelpers do 2 | @moduledoc """ 3 | Checkers for Payload options 4 | 5 | https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v3.4.pdf 6 | """ 7 | 8 | @doc """ 9 | Returns {:ok, ip_address} or {:error, reason} for IP address validity 10 | 11 | ## Examples 12 | iex> ExBankID.PayloadHelpers.check_ip_address("1.1.1.1") 13 | {:ok, "1.1.1.1"} 14 | 15 | iex> ExBankID.PayloadHelpers.check_ip_address("345.0.0.0") 16 | {:error, "Invalid ip address: 345.0.0.0"} 17 | """ 18 | def check_ip_address(ip_address) 19 | when is_binary(ip_address) do 20 | ip_address_cl = String.to_charlist(ip_address) 21 | 22 | case :inet.parse_strict_address(ip_address_cl) do 23 | {:ok, _} -> 24 | {:ok, ip_address} 25 | 26 | _ -> 27 | {:error, "Invalid ip address: #{ip_address}"} 28 | end 29 | end 30 | 31 | @doc """ 32 | Returns {:ok, personal_number}, {:ok, nil} or {:error, reason} for personal number 33 | 34 | ## Examples 35 | iex> ExBankID.PayloadHelpers.check_personal_number("190000000000") 36 | {:ok, "190000000000"} 37 | 38 | iex> ExBankID.PayloadHelpers.check_personal_number("42") 39 | {:error, "Invalid personal number: 42"} 40 | 41 | iex> ExBankID.PayloadHelpers.check_personal_number(nil) 42 | {:ok, nil} 43 | """ 44 | def check_personal_number(personal_number) 45 | when is_binary(personal_number) do 46 | # TODO: Implement relevant personal number check. 47 | case String.length(personal_number) do 48 | 12 -> 49 | {:ok, personal_number} 50 | 51 | _ -> 52 | {:error, "Invalid personal number: #{personal_number}"} 53 | end 54 | end 55 | 56 | def check_personal_number(nil) do 57 | {:ok, nil} 58 | end 59 | 60 | @doc """ 61 | Returns: 62 | * {:ok, nil} when requirement is omitted 63 | * {:ok, requirement} when all requirement's keys are valid 64 | * {:error, "Invalid requirement"} when requirement is anything besides a map or nil 65 | 66 | ## Examples 67 | iex> ExBankID.PayloadHelpers.check_requirement(nil) 68 | {:ok, nil} 69 | 70 | iex> ExBankID.PayloadHelpers.check_requirement(%{allowFingerprint: :true, cardReader: "class1"}) 71 | {:ok, %{allowFingerprint: :true, cardReader: "class1"}} 72 | 73 | iex> ExBankID.PayloadHelpers.check_requirement(%{tokenStartRequired: :false, notRealRequirement: "fails"}) 74 | {:error, "Invalid requirement"} 75 | 76 | iex> ExBankID.PayloadHelpers.check_requirement("something") 77 | {:error, "Invalid requirement"} 78 | """ 79 | def check_requirement(nil), do: {:ok, nil} 80 | 81 | def check_requirement(%{} = requirement) do 82 | if Enum.all?( 83 | Enum.map( 84 | Map.keys(requirement), 85 | fn key -> check_requirement(key, requirement[key]) end 86 | ), 87 | fn result -> result == {:ok, nil} end 88 | ) do 89 | {:ok, requirement} 90 | else 91 | {:error, "Invalid requirement"} 92 | end 93 | end 94 | 95 | def check_requirement(_), do: {:error, "Invalid requirement"} 96 | 97 | def check_requirement(:allowFingerprint, value) when is_boolean(value), do: {:ok, nil} 98 | 99 | @doc """ 100 | Returns 101 | * {:ok, nil} when autoStartTokenRequired is a boolean 102 | * {:ok, nil} when cardReader is "class1" or "class2" 103 | * {:error, "Invalid requirement"} when certificatePolicies is an empty list 104 | * {:ok, nil} when certificatePolicies is a list of oid's 105 | * {:error, "Invalid requirement"} when issuerCn is an empty list 106 | * {:ok, nil} when issuerCn is a list of strings 107 | * {:ok, nil} when tokenStartRequired is a boolean 108 | * {:error, "Invalid requirement"} when key, value is anything else besides above permitted 109 | 110 | ## Examples 111 | iex> ExBankID.PayloadHelpers.check_requirement(:autoStartTokenRequired, :false) 112 | {:ok, nil} 113 | 114 | iex> ExBankID.PayloadHelpers.check_requirement(:autoStartTokenRequired, "true") 115 | {:error, "Invalid requirement"} 116 | 117 | iex> ExBankID.PayloadHelpers.check_requirement(:cardReader, "class1") 118 | {:ok, nil} 119 | 120 | iex> ExBankID.PayloadHelpers.check_requirement(:cardReader, "class3") 121 | {:error, "Invalid requirement"} 122 | 123 | iex> ExBankID.PayloadHelpers.check_requirement(:certificatePolicies, []) 124 | {:error, "Invalid requirement"} 125 | 126 | iex> ExBankID.PayloadHelpers.check_requirement(:certificatePolicies, ["1.2.752.78.*", "1.2.752.78.1.5"]) 127 | {:ok, nil} 128 | 129 | iex> ExBankID.PayloadHelpers.check_requirement(:certificatePolicies, ["not an oid"]) 130 | {:error, "Invalid requirement"} 131 | 132 | iex> ExBankID.PayloadHelpers.check_requirement(:certificatePolicies, []) 133 | {:error, "Invalid requirement"} 134 | 135 | iex> ExBankID.PayloadHelpers.check_requirement(:issuerCn, ["Nordea CA for Smartcard users 12", "Nordea Test CA for Softcert users 13"]) 136 | {:ok, nil} 137 | 138 | iex> ExBankID.PayloadHelpers.check_requirement(:issuerCn, "Nordea Test CA for Softcert users 13") 139 | {:error, "Invalid requirement"} 140 | 141 | iex> ExBankID.PayloadHelpers.check_requirement(:tokenStartRequired, :false) 142 | {:ok, nil} 143 | 144 | iex> ExBankID.PayloadHelpers.check_requirement(:tokenStartRequired, "true") 145 | {:error, "Invalid requirement"} 146 | 147 | iex> ExBankID.PayloadHelpers.check_requirement(:foo, "bar") 148 | {:error, "Invalid requirement"} 149 | """ 150 | def check_requirement(:autoStartTokenRequired, value) when is_boolean(value), do: {:ok, nil} 151 | 152 | def check_requirement(:cardReader, value) when is_binary(value) do 153 | case value do 154 | "class1" -> {:ok, nil} 155 | "class2" -> {:ok, nil} 156 | _ -> {:error, "Invalid requirement"} 157 | end 158 | end 159 | 160 | def check_requirement(:certificatePolicies, []), do: {:error, "Invalid requirement"} 161 | 162 | def check_requirement(:certificatePolicies, value) when is_list(value) do 163 | if Enum.all?(value, fn oid -> oid?(oid) end) do 164 | {:ok, nil} 165 | else 166 | {:error, "Invalid requirement"} 167 | end 168 | end 169 | 170 | def check_requirement(:issuerCn, []), do: {:error, "Invalid requirement"} 171 | 172 | def check_requirement(:issuerCn, value) when is_list(value) do 173 | if Enum.all?(value, fn cn -> is_binary(cn) end) do 174 | {:ok, nil} 175 | else 176 | {:error, "Invalid requirement"} 177 | end 178 | end 179 | 180 | def check_requirement(:tokenStartRequired, value) when is_boolean(value), do: {:ok, nil} 181 | 182 | def check_requirement(_, _), do: {:error, "Invalid requirement"} 183 | 184 | @doc """ 185 | Returns true when value is an oid 186 | 187 | One wildcard "*" is allowed from position 5 and forward ie. 1.2.752.78.* 188 | 189 | ## Examples 190 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.78.1.1") 191 | true 192 | 193 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.78.1.2") 194 | true 195 | 196 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.78.1.5") 197 | true 198 | 199 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.71.1.3") 200 | true 201 | 202 | iex> ExBankID.PayloadHelpers.oid?("1.2.3.4.5") 203 | true 204 | 205 | iex> ExBankID.PayloadHelpers.oid?("1.2.3.4.10") 206 | true 207 | 208 | iex> ExBankID.PayloadHelpers.oid?("1.2.3.4.25") 209 | true 210 | 211 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.71.1.3") 212 | true 213 | 214 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.60.1.6") 215 | true 216 | 217 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.*.1.6") 218 | false 219 | 220 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.60.*") 221 | true 222 | 223 | iex> ExBankID.PayloadHelpers.oid?("1.2.752.60.1.*") 224 | true 225 | """ 226 | def oid?(value) when is_binary(value) do 227 | value =~ ~r/^([1-9][0-9]{0,3}|0)(\.([1-9][0-9]{0,3}|0)){5,13}$/ or 228 | value =~ ~r/^([1-9][0-9]{0,3}|0)(\.([1-9][0-9]{0,3}|0)){3}(\.([1-9][0-9]{0,3}|0|\*)){0,9}$/ 229 | end 230 | 231 | def oid?(_), do: false 232 | end 233 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, 4 | "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, 5 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 6 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, 7 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 8 | "credo": {:hex, :credo, "1.5.3", "f345253655f2efe1e4693a03437606462681e91303ebc9e3909c14268effc37a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f7e238c10051cc22515e3f75754200b567d93c00d93be81fc59d47bc3dfdc5be"}, 9 | "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, 10 | "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, 11 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 12 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 13 | "excoveralls": {:hex, :excoveralls, "0.13.3", "edc5f69218f84c2bf61b3609a22ddf1cec0fbf7d1ba79e59f4c16d42ea4347ed", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cc26f48d2f68666380b83d8aafda0fffc65dafcc8d8650358e0b61f6a99b1154"}, 14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 15 | "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"}, 16 | "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, 17 | "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, 18 | "iptools": {:hex, :iptools, "0.0.2", "cd7673c4ae9063e4d3e7368a4f94d59c5b3fc79de0a0f3f2ca4b5015b1e4ddec", [:mix], [], "hexpm", "33bf27bc72094bbc4e67c664c979e5cebfe17c5369c91fc2e2610cc726b252db"}, 19 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 20 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 21 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, 22 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 23 | "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, 24 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 25 | "nimble_options": {:hex, :nimble_options, "0.3.5", "a4f6820cdcb4ee444afd78635f323e58e8a5ddf2fbbe9b9d283a99f972034bae", [:mix], [], "hexpm", "f5507cc90033a8d12769522009c80aa9164af6bab245dbd4ad421d008455f1e1"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 27 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 28 | "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [: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: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, 29 | "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, 30 | "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, 31 | "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, 32 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 33 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 34 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 35 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, 36 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, 37 | } 38 | --------------------------------------------------------------------------------