mtchavez / ueberauth_gitlab

Gitlab OAuth2 Strategy fo Überauth
Other
7 stars 6 forks source link

Unable to authenticate against GitLab OAuth #15

Closed nathanchere closed 5 years ago

nathanchere commented 5 years ago

The handover to GitLab works fine but on handling the callback throws this:

The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed

Looks like I'm not alone with it:

https://elixirforum.com/t/implementing-ueberauth-gitlab/15128

I'll look into it but leaving this here in case it's an already known and/or resolved issue.

nathanchere commented 5 years ago
OAuth2.Error at GET /signin/gitlab/callback
Server responded with status: 401
Headers:

server: nginx
date: Tue, 23 Oct 2018 13:12:56 GMT
content-type: application/json; charset=utf-8
content-length: 162
cache-control: no-store
pragma: no-cache
www-authenticate: Bearer realm="Doorkeeper", error="invalid_request", error_description="The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-request-id: 6c723674-d6df-41d2-a738-b71deb02f7a1
x-runtime: 0.019778
x-xss-protection: 1; mode=block
content-security-policy: object-src 'none'; worker-src https://assets.gitlab-static.net https://gl-canary.global.ssl.fastly.net https://gitlab.com blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://assets.gitlab-static.net https://gl-canary.global.ssl.fastly.net https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://apis.google.com; style-src 'self' 'unsafe-inline' https://assets.gitlab-static.net https://gl-canary.global.ssl.fastly.net; img-src * data: blob:; frame-src 'self' https://www.google.com/recaptcha/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com https://*.codesandbox.io; frame-ancestors 'self'; connect-src 'self' https://assets.gitlab-static.net https://gl-canary.global.ssl.fastly.net wss://gitlab.com https://sentry.gitlab.net https://customers.gitlab.com https://snowplow.trx.gitlab.net

Body:

%{"error" => "invalid_request", "error_description" => "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."}
nathanchere commented 5 years ago
Call stack:

 oauth2 lib/oauth2/client.ex:250 OAuth2.Client.get_token!/4
 ueberauth_gitlab_strategy lib/ueberauth/strategy/gitlab/oauth.ex:66 Ueberauth.Strategy.Gitlab.OAuth.get_token!/2
 ueberauth_gitlab_strategy lib/ueberauth/strategy/gitlab.ex:113 Ueberauth.Strategy.Gitlab.handle_callback!/1
 ueberauth lib/ueberauth/strategy.ex:301 Ueberauth.Strategy.run_callback/2
 lib/ninja_portal_web/controllers/auth_controller.ex:1 NinjaPortalWeb.AuthController.phoenix_controller_pipeline/1
nathanchere commented 5 years ago

As far as how I'm configuring it:

config :ueberauth, Ueberauth,
  providers: [
      gitlab: {Ueberauth.Strategy.Gitlab, [uid_field: :username, default_scope: "read_user"]},
...
mtchavez commented 5 years ago

Looks like some missing documentation possibly. I updated the README with a known working configuration. Please try that out and let me know if that fixes your issue. The update is basically this:

config :ueberauth, Ueberauth,
  providers: [
    identity: { Ueberauth.Strategy.Identity, [
        callback_methods: ["POST"],
        uid_field: :email,
        nickname_field: :username,
      ] },
    gitlab: {Ueberauth.Strategy.Gitlab, [default_scope: "read_user"]},
  ]
nathanchere commented 5 years ago

The only difference there to what I have in my gitlab provider settings is not including the uid_field: :username pair, and it doesn't make a difference to the response from GitLab (i.e. The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed)

nathanchere commented 5 years ago

Requests are generated in this format:

https://gitlab.com/oauth/authorize?client_id=abcd12345678abcd&redirect_uri=http%3A%2F%2Flocalhost%3A1337%2Fsignin%2Fgitlab%2Fcallback&response_type=code&scope=read_user

Looking at the Gitlab OAuth2 provider documentation, the format for the token based flow is pretty similar, e.g.:

https://gitlab.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=token&state=YOUR_UNIQUE_STATE_HASH

and I change the outgoing response type to token instead of code, I at least get a valid redirect back. But that's pretty much the only thing I can change (other than adding a state parameter) which doesn't break the request.

mtchavez commented 5 years ago

I'd like to be able to offer some help for you on this. Is there any way you can provide a sample app that reproduces this issue? I have been using a working configuration for some time now so I suspect something is different in your whole stack for versions of elixir, OTP, dependencies etc that could be exposing this.

Regarding changing the response type to token instead of code, that is curious because the RFC for web application flow, linked to from the Gitlab docs, explicitly states response type is required to be code.

mtchavez commented 5 years ago

Closing due to inactivity. Please let me know if you have any updates or issues.

Exadra37 commented 4 years ago

I am experience the same exact issue:

[error] #PID<0.776.0> running TasksWeb.Endpoint (connection #PID<0.744.0>, stream id 8) terminated
Server: localhost:2000 (http)
Request: GET /auth/gitlab/callback?code=zzzzzzzzzzzzzzzzzz&state=myprivatestate
** (exit) an exception was raised:
    ** (OAuth2.Error) Server responded with status: 401

Headers:

date: Sat, 18 Apr 2020 11:56:39 GMT
content-type: application/json; charset=utf-8
content-length: 162
connection: keep-alive
set-cookie: __cfduid=yyyyyyyyyyyyyyyy; expires=Mon, 18-May-20 11:56:38 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; Secure
cache-control: private, no-store
pragma: no-cache
www-authenticate: Bearer realm="Doorkeeper", error="invalid_request", error_description="The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."
x-request-id: HLG2JJGfoma
x-runtime: 0.018386
gitlab-lb: fe-21-lb-gprd
gitlab-sv: web-33-sv-gprd
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 585e3425c9e1d224-MAN
cf-request-id: 022ebeeba00000d22491948200000001

Body:

"{\"error\":\"invalid_request\",\"error_description\":\"The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.\"}"

        (oauth2) lib/oauth2/client.ex:298: OAuth2.Client.get_token!/4
        (ueberauth_gitlab_strategy) lib/ueberauth/strategy/gitlab/oauth.ex:71: Ueberauth.Strategy.Gitlab.OAuth.get_token!/2
        (ueberauth_gitlab_strategy) lib/ueberauth/strategy/gitlab.ex:116: Ueberauth.Strategy.Gitlab.handle_callback!/1
        (ueberauth) lib/ueberauth/strategy.ex:307: Ueberauth.Strategy.run_callback/2
        (tasks) TasksWeb.Router.auth/2
        (tasks) lib/tasks_web/router.ex:1: TasksWeb.Router.__pipe_through0__/1
        (phoenix) lib/phoenix/router.ex:283: Phoenix.Router.__call__/2
        (tasks) lib/tasks_web/endpoint.ex:1: TasksWeb.Endpoint.plug_builder_call/2
        (tasks) lib/plug/debugger.ex:132: TasksWeb.Endpoint."call (overridable 3)"/2
        (tasks) lib/tasks_web/endpoint.ex:1: TasksWeb.Endpoint.call/2
        (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4
        (cowboy) /home/developer/workspace/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
        (cowboy) /home/developer/workspace/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3
        (cowboy) /home/developer/workspace/deps/cowboy/src/cowboy_stream_h.erl:302: :cowboy_stream_h.request_process/3
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/

My config:

# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
use Mix.Config

config :tasks,
  ecto_repos: [Tasks.Repo]

# Configures the endpoint
config :tasks, TasksWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: "xxxxxxxxxxxxx",
  render_errors: [view: TasksWeb.ErrorView, accepts: ~w(html json)],
  pubsub: [name: Tasks.PubSub, adapter: Phoenix.PubSub.PG2],
  live_view: [
    signing_salt: "yyyyyyyyyyyyyyyyyyyyyyy"
  ]

config :ueberauth, Ueberauth,
  providers: [
    github: {Ueberauth.Strategy.Github, [default_scope: "user:email"]},
    gitlab: {Ueberauth.Strategy.Gitlab, [default_scope: "read_user", api_version: "v4"]}
  ]

config :ueberauth, Ueberauth.Strategy.Github.OAuth,
  client_id: System.get_env("GITHUB_CLIENT_ID"),
  client_secret: System.get_env("GITHUB_CLIENT_SECRET")

config :ueberauth, Ueberauth.Strategy.Gitlab.OAuth,
  client_id: System.get_env("GITLAB_CLIENT_ID"),
  client_secret: System.get_env("GITLAB_CLIENT_SECRET")

# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

config :mnesia,
  dir: '.mnesia/#{Mix.env}/#{node()}'

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

My router:

defmodule TasksWeb.Router do
  use TasksWeb, :router

  require Logger

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    #plug :fetch_flash
    plug :fetch_live_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :assign_current_user
    plug :put_root_layout, {TasksWeb.LayoutView, :root}
    plug Plug.Validator, on_error: &TasksWeb.Router.validation_error_callback/2
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  pipeline :require_login do
    plug :check_user_is_logged
  end

  pipeline :auth do
    plug Ueberauth,
      otp_app: :tasks
  end

  scope "/auth", TasksWeb do
    pipe_through [:browser, :auth]

    get "/login", AuthController, :login
    get "/:provider", AuthController, :request
    get "/:provider/callback", AuthController, :callback
    post "/:provider/callback", AuthController, :callback
    post "/logout", AuthController, :logout
  end

  scope "/", TasksWeb do
    pipe_through [:browser]

    get "/", PageController, :index
  end

  scope "/", TasksWeb do
    pipe_through [:browser, :require_login]

    live "/todos", TodoLive
    live "/todos/:date", TodoLive, private: %{validate: %{date: &Utils.Validators.Date.valid_iso8601?/1}}
  end

  def validation_error_callback(conn, errors) do
    Logger.error "#{__MODULE__} failed to validate route #{conn.request_path} params: #{inspect(errors)}"

    conn
    |> put_status(:not_found)
    |> put_view(TasksWeb.ErrorView)
    |> render("404.html")
    |> halt()
  end

  # Fetch the current user from the session and add it to `conn.assigns`. This
  # will allow you to have access to the current user in your views with
  # `@current_user`.
  defp assign_current_user(conn, _) do
    assign(conn, :current_user, get_session(conn, :current_user))
  end

  defp check_user_is_logged(conn, _) do
    case get_session(conn, :current_user) do
      nil ->
        conn
        |> put_flash(:info, "Please login to access this resource!")
        |> redirect(to: "/auth/login")
        |> halt()
      _user ->
        conn
    end
  end

  # Other scopes may use custom stacks.
  # scope "/api", TasksWeb do
  #   pipe_through :api
  # end
end

My controller:

defmodule TasksWeb.AuthController do
  @moduledoc """
  Auth controller responsible for handling Ueberauth responses
  """

  use TasksWeb, :controller
  #plug Ueberauth

  #alias Ueberauth.Strategy.Helpers

  def login(conn, _params) do
    render(conn, "login.html")
  end

  def logout(conn, _params) do
    conn
    |> put_flash(:info, "You have been logged out!")
    |> clear_session()
    |> redirect(to: "/")
  end

  def callback(%{assigns: %{ueberauth_failure: _fails} = fail} = conn, _params) do
IO.inspect fail, label: "FAILED AUTH"
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: "/auth/login")
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    case Tasks.UserFromAuth.find_or_create(auth) do
      {:ok, user} ->
        conn
        |> put_session(:current_user, user)
        |> configure_session(renew: true)
        |> redirect(to: "/")

      {:error, reason} ->
        conn
        |> put_flash(:error, reason)
        |> redirect(to: "/auth/login")
    end
  end
end

This works for the Github strategy, but doesn't work for the Gitlab strategy, and after digging into the code of your Library I was not able to figure out why, but I am also a newbie in Elixir.

It looks the request gets blocked by CloudFare due to the request not conforming exactly with the definition provided by Gitlab.

Do you have any insights why this may be happening?