basic_auth.ex (5283B)
1 defmodule Plug.BasicAuth do 2 @moduledoc """ 3 Functionality for providing Basic HTTP authentication. 4 5 It is recommended to only use this module in production 6 if SSL is enabled and enforced. See `Plug.SSL` for more 7 information. 8 9 ## Compile-time usage 10 11 If you have a single username and password, you can use 12 the `basic_auth/2` plug: 13 14 import Plug.BasicAuth 15 plug :basic_auth, username: "hello", password: "secret" 16 17 Or if you would rather put those in a config file: 18 19 # lib/your_app.ex 20 import Plug.BasicAuth 21 plug :basic_auth, Application.compile_env(:my_app, :basic_auth) 22 23 # config/config.exs 24 config :my_app, :basic_auth, username: "hello", password: "secret" 25 26 Once the user first accesses the page, the request will be denied 27 with reason 401 and the request is halted. The browser will then 28 prompt the user for username and password. If they match, then the 29 request succeeds. 30 31 Both approaches shown above rely on static configuration. Let's see 32 alternatives. 33 34 ## Runtime-time usage 35 36 As any other Plug, we can use the `basic_auth` at runtime by simply 37 wrapping it in a function: 38 39 plug :auth 40 41 defp auth(conn, opts) do 42 username = System.fetch_env!("AUTH_USERNAME") 43 password = System.fetch_env!("AUTH_PASSWORD") 44 Plug.BasicAuth.basic_auth(conn, username: username, password: password) 45 end 46 47 This approach is useful when both username and password are specified 48 upfront and available at runtime. However, you may also want to compute 49 a different password for each different user. In those cases, we can use 50 the low-level API. 51 52 ## Low-level usage 53 54 If you want to provide your own authentication logic on top of Basic HTTP 55 auth, you can use the low-level functions. As an example, we define `:auth` 56 plug that extracts username and password from the request headers, compares 57 them against the database, and either assigns a `:current_user` on success 58 or responds with an error on failure. 59 60 plug :auth 61 62 defp auth(conn, _opts) do 63 with {user, pass} <- Plug.BasicAuth.parse_basic_auth(conn), 64 %User{} = user <- MyApp.Accounts.find_by_username_and_password(user, pass) do 65 assign(conn, :current_user, user) 66 else 67 _ -> conn |> Plug.BasicAuth.request_basic_auth() |> halt() 68 end 69 end 70 71 Keep in mind that: 72 73 * The supplied `user` and `pass` may be empty strings; 74 75 * If you are comparing the username and password with existing strings, 76 do not use `==/2`. Use `Plug.Crypto.secure_compare/2` instead. 77 78 """ 79 import Plug.Conn 80 81 @doc """ 82 Higher level usage of Basic HTTP auth. 83 84 See the module docs for examples. 85 86 ## Options 87 88 * `:username` - the expected username 89 * `:password` - the expected password 90 * `:realm` - the authentication realm. The value is not fully 91 sanitized, so do not accept user input as the realm and use 92 strings with only alphanumeric characters and space 93 94 """ 95 def basic_auth(conn, options \\ []) do 96 username = Keyword.fetch!(options, :username) 97 password = Keyword.fetch!(options, :password) 98 99 with {request_username, request_password} <- parse_basic_auth(conn), 100 valid_username? = Plug.Crypto.secure_compare(username, request_username), 101 valid_password? = Plug.Crypto.secure_compare(password, request_password), 102 true <- valid_username? and valid_password? do 103 conn 104 else 105 _ -> conn |> request_basic_auth(options) |> halt() 106 end 107 end 108 109 @doc """ 110 Parses the request username and password from Basic HTTP auth. 111 112 It returns either `{user, pass}` or `:error`. Note the username 113 and password may be empty strings. When comparing the username 114 and password with the expected values, be sure to use 115 `Plug.Crypto.secure_compare/2`. 116 117 See the module docs for examples. 118 """ 119 def parse_basic_auth(conn) do 120 with ["Basic " <> encoded_user_and_pass] <- get_req_header(conn, "authorization"), 121 {:ok, decoded_user_and_pass} <- Base.decode64(encoded_user_and_pass), 122 [user, pass] <- :binary.split(decoded_user_and_pass, ":") do 123 {user, pass} 124 else 125 _ -> :error 126 end 127 end 128 129 @doc """ 130 Encodes a basic authentication header. 131 132 This can be used during tests: 133 134 put_req_header(conn, "authorization", encode_basic_auth("hello", "world")) 135 136 """ 137 def encode_basic_auth(user, pass) when is_binary(user) and is_binary(pass) do 138 "Basic " <> Base.encode64("#{user}:#{pass}") 139 end 140 141 @doc """ 142 Requests basic authentication from the client. 143 144 It sets the response to status 401 with "Unauthorized" as body. 145 The response is not sent though (nor the connection is halted), 146 allowing developers to further customize it. 147 148 ## Options 149 150 * `:realm` - the authentication realm. The value is not fully 151 sanitized, so do not accept user input as the realm and use 152 strings with only alphanumeric characters and space 153 """ 154 def request_basic_auth(conn, options \\ []) when is_list(options) do 155 realm = Keyword.get(options, :realm, "Application") 156 escaped_realm = String.replace(realm, "\"", "") 157 158 conn 159 |> put_resp_header("www-authenticate", "Basic realm=\"#{escaped_realm}\"") 160 |> resp(401, "Unauthorized") 161 end 162 end