Search code examples
elixirphoenix-frameworkguardianueberauthguardian-db

Problems with guardian - how generate access and refresh token in login


To invalidate JWTs, people use one of two methods

  • blacklist/whitelist (with guardian_db).
  • a refresh token (which allows regenerating access tokens) with a short expiring access token.

I dont want to use guardian_db in my project. So, how i generate access token and refresh token in the login endpoint?

My code is:

File mix.ex

    # Authentication library
    {:guardian, "~> 1.0"}

File config.exs

    # Guardian configuration
    config :my_proj, MyProj.Guardian,
        issuer: "my_proj",
        verify_module: Guardian.JWT,
        secret_key: "Xxxxxxxxxxxxxxxxxxxxxxxxxx",
        allowed_drift: 2000,
        verify_issuer: true,
        ttl: {5, :minutes}

    # Ueberauth configuration
    config :ueberauth, Ueberauth,
        base_path: "/api/auth",
        providers: [
        identity: {
            Ueberauth.Strategy.Identity,
            [
                callback_methods: ["POST"],
                callback_path: "/api/auth/login",
                nickname_field: :email,
                param_nesting: "account",
                uid_field: :email
            ]
            }
        ]

File aut_access_pipeline.ex

    defmodule MyProjWeb.Plug.AuthAccessPipeline do
        use Guardian.Plug.Pipeline, otp_app: :my_proj

        plug(Guardian.Plug.VerifySession, claims: %{"typ" => "access"})
        plug(Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"})
        plug(Guardian.Plug.EnsureAuthenticated)
        plug(Guardian.Plug.LoadResource, ensure: true)
        plug(MyProjWeb.Plug.CurrentUser)
        plug(MyProjWeb.Plug.CheckToken)
    end

File auth_controller.ex

    defmodule MyProjWeb.AuthenticationController do
        def login(conn, %{"email" => email, "password" => password}) do
            case Accounts.get_user_by_email_and_password(email, password) do
                {:ok, user} ->
                    handle_login_response(conn, user, "login.json")

                {:error, _reason} ->
                    conn
                    |> put_status(:unauthorized)
                    |> Error.render(:invalid_credentials)
                    |> halt
            end
        end

        def refresh(conn, %{"jwt" => existing_jwt}) do
            case MyProj.Guardian.refresh(existing_jwt, ttl: {5, :minutes}) do
                {:ok, {_old_token, _old_claims}, {new_jwt, _new_claims}} ->
                    current_user = current_resource(conn)
                    user = MyProj.Accounts.get_user!(current_user.id)

                    conn
                    |> put_resp_header("authorization", "Bearer #{new_jwt}")
                    |> render(MyProjWeb.AuthenticationView, json_file, %{jwt: new_jwt, user: user})
                {:error, _reason} ->
                    conn
                    |> put_status(:unauthorized)
                    |> Error.render(:invalid_credentials)
            end
        end

        defp handle_login_response(conn, user, json_file) do
            new_conn = MyProj.Guardian.Plug.sign_in(conn, user, [])
            jwt = MyProj.Guardian.Plug.current_token(new_conn)
            %{"exp" => exp} = MyProj.Guardian.Plug.current_claims(new_conn)

            new_conn
            |> put_resp_header("authorization", "Bearer #{jwt}")
            |> put_resp_header("x-expires", "#{exp}")
            |> render(MyProjWeb.AuthenticationView, json_file, %{jwt: jwt, user: user})
        end
    end
end

File guardian.ex

    defmodule MyProj.Guardian do
        use Guardian, otp_app: :my_proj

        def subject_for_token(user = %User{}, _claims),
            do: {:ok, "User:#{user.id}"}

        def subject_for_token(_user, _claims) do
            {:error, :no_resource_id}
        end

        def resource_from_claims(%{"sub" => "User:" <> id}) do
            {:ok, MyProj.Accounts.get_user!(id)}
        end

        def resource_from_claims(_claims) do
            {:error, :no_claims_sub}
        end
    end

With this code i have a valid token for access but i dont know how generate the refresh token for regenerate/refresh the access token when he expires...

anyone can help?


Solution

  • My solution is something like this...

    In the login endpoint i respond with 2 tokens (access and refresh) ...

    new_conn =
      MyProj.Guardian.Plug.sign_in(
        conn,
        credentials,
        %{},
        token_type: "refresh"
      )
    
    jwt_refresh = MyProj.Guardian.Plug.current_token(new_conn)
    
    {:ok, _old_stuff, {jwt, %{"exp" => exp} = _new_claims}} =
      MyProj.Guardian.exchange(jwt_refresh, "refresh", "access")
    

    Basically the solution is to use the MyProj.Guardian.exchange(...)

    In the refresh endpoint i respond with the new access token ...

    def refresh(conn, %{"jwt_refresh" => jwt_refresh}) do
    case MyProj.Guardian.exchange(jwt_refresh, "refresh", "access") do
      {:ok, _old_stuff, {jwt, %{"exp" => exp} = _new_claims}} ->
        conn
        |> put_resp_header("authorization", "Bearer #{jwt}")
        |> put_resp_header("x-expires", "#{exp}")
        |> render(MyProjWeb.AuthenticationView, "refresh.json", %{jwt: jwt})
    
      {:error, _reason} ->
        conn
        |> put_status(:unauthorized)
        |> Error.render(:invalid_credentials)
    end
    end