defmodule AuthenticationError do defexception [:message] def exception(_) do %AuthenticationError{message: "No code provided by Spotify. Authorize your app again"} end end defmodule Spotify.Authentication do @moduledoc """ Authenticates the Spotify user. After your app is authorized, the user must be authenticated. A redirect URL is specified in the config folder. This is the URL that Spotify redirects to after authorization, and should ultimately end up hitting this module's `authenticate` function. If the authorization is successful, the param `code` will be present. If a refresh token still exists, the client will refresh the access token. You have the option to pass either a Plug.Conn or a Spotify.Credentials struct into these functions. If you pass Conn, the auth tokens will be saved in cookies. If you pass Credentials, you will be responsible for persisting the auth tokens between requests. """ alias Spotify.{Credentials,Cookies} @doc """ Authenticates the user The authorization code must be present from spotify or an exception will be raised. The token will be refreshed if possible, otherwise the app will request new access and request tokens. ## Example: ## Spotify.authenticate(conn, %{"code" => code}) # {:ok, conn} Spotify.authenticate(conn, %{"not_a_code" => invalid}) # AuthenticationError, "No code provided by Spotify. Authorize your app again" Spotify.authenticate(auth, params) # {:ok, auth} """ def authenticate(conn_or_auth, map) def authenticate(conn = %Plug.Conn{}, params) do {:ok, auth} = conn |> Credentials.new |> authenticate(params) {:ok, Cookies.set_cookies(conn, auth)} end def authenticate(auth, %{"code" => code}) do auth |> body_params(code) |> AuthenticationClient.post end def authenticate(_, _) do raise AuthenticationError, "No code provided by Spotify. Authorize your app again" end @doc """ Attempts to refresh your access token if the refresh token exists. Returns `:unauthorized` if there is no refresh token. """ def refresh(conn_or_auth) def refresh(conn = %Plug.Conn{}) do with {:ok, auth} <- conn |> Credentials.new |> refresh do {:ok, Cookies.set_cookies(conn, auth)} end end def refresh(%Credentials{refresh_token: nil}), do: :unauthorized def refresh(auth), do: auth |> body_params |> AuthenticationClient.post @doc """ Checks for refresh and access tokens ## Example: ## defmodule PlayListController do plug :check_tokens def check_tokens do unless Spotify.Authentication.tokens_present?(conn) do redirect conn, to: authorization_path(:authorize) end end end """ def tokens_present?(conn_or_auth) def tokens_present?(%Credentials{access_token: nil}), do: false def tokens_present?(%Credentials{refresh_token: nil}), do: false def tokens_present?(%Credentials{}), do: true def tokens_present?(conn), do: conn |> Credentials.new |> tokens_present? @doc false def authenticated?(%Credentials{access_token: token}), do: token def authenticated?(conn), do: conn |> Credentials.new |> authenticated? @doc false def body_params(%Credentials{refresh_token: token}) do "grant_type=refresh_token&refresh_token=#{token}" end @doc false def body_params(%Credentials{refresh_token: nil}, code) do "grant_type=authorization_code&code=#{code}&redirect_uri=#{Spotify.callback_url}" end def body_params(auth, _code), do: body_params(auth) end