README.md

# KeenAuth

A powerful and flexible OAuth authentication library for Phoenix applications. KeenAuth provides a clean pipeline architecture that separates concerns into three main components: Strategy, Mapper, and Storage.

## Architecture Overview

KeenAuth follows a pipeline approach where each authentication stage can be customized independently:

```
User → Strategy → Mapper → Processor → Storage
       (OAuth)   (Normalize) (Business Logic) (Persist)
```

### Core Components

- **Strategy**: Handles provider-specific OAuth protocols (Azure AD, GitHub, Facebook, etc.)
- **Mapper**: Normalizes provider responses and can enrich data with additional API calls
- **Processor**: Implements business logic, user validation, and transformations
- **Storage**: Manages user data persistence (sessions, database, JWT, etc.)

## Basic Flow Diagram

```mermaid
flowchart LR
    A[Strategy<br/>OAuth flow<br/>Token exchange] --> B[Mapper<br/>Normalize<br/>user data]
    B --> C[Processor<br/>Business logic<br/>validation]
    C --> D[Storage<br/>Persist<br/>user data]
```

## Advanced Example: Azure AD with Graph API

### Pipeline Flow
```mermaid
flowchart LR
    A[Strategy<br/>Azure AD OAuth<br/>Get tokens] --> B[Mapper<br/>• Normalize fields<br/>• Call Graph API<br/>• Fetch user groups<br/>• Get manager info]
    B --> C[Processor<br/>• Validate domain<br/>• Check permissions<br/>• Create/update user<br/>• Assign roles]
    C --> D[Storage<br/>Database +<br/>Session +<br/>JWT token]
```

### Detailed Interaction Flow
```mermaid
sequenceDiagram
    participant User
    participant KeenAuth
    participant AzureAD
    participant GraphAPI
    participant Database
    participant Session

    User->>KeenAuth: Login request
    KeenAuth->>AzureAD: Redirect to OAuth
    AzureAD->>User: Login form
    User->>AzureAD: Credentials
    AzureAD->>KeenAuth: OAuth callback with code

    Note over KeenAuth: Strategy Phase
    KeenAuth->>AzureAD: Exchange code for tokens
    AzureAD->>KeenAuth: Access token + User info

    Note over KeenAuth: Mapper Phase
    KeenAuth->>GraphAPI: Get user groups (with token)
    GraphAPI->>KeenAuth: User groups
    KeenAuth->>GraphAPI: Get manager info
    GraphAPI->>KeenAuth: Manager details
    KeenAuth->>GraphAPI: Get user photo
    GraphAPI->>KeenAuth: Profile photo

    Note over KeenAuth: Processor Phase
    KeenAuth->>Database: Check/create user
    Database->>KeenAuth: User record
    KeenAuth->>Database: Update user roles
    Database->>KeenAuth: Updated user

    Note over KeenAuth: Storage Phase
    KeenAuth->>Database: Store user session
    KeenAuth->>Session: Set session data
    KeenAuth->>User: Set JWT cookie
    KeenAuth->>User: Redirect to app
```

## Installation

Add `keen_auth` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:keen_auth, "~> 1.0"}
  ]
end
```

## Quick Start

### 1. Configuration

Add to your `config.exs`:

> [!IMPORTANT]
> **OAuth Scopes**: KeenAuth automatically requests `openid profile email offline_access` scopes if you don't specify any. This ensures user profile data is returned by the provider. If you specify custom scopes via `authorization_params: [scope: "..."]`, make sure to include at least `openid profile email` or you may receive empty user data.

```elixir
config :keen_auth,
  strategies: [
    azure_ad: [
      strategy: Assent.Strategy.AzureAD,
      mapper: KeenAuth.Mappers.AzureAD,
      processor: MyApp.Auth.Processor,
      config: [
        tenant_id: System.get_env("AZURE_TENANT_ID"),
        client_id: System.get_env("AZURE_CLIENT_ID"),
        client_secret: System.get_env("AZURE_CLIENT_SECRET"),
        redirect_uri: "https://myapp.com/auth/azure_ad/callback"
      ]
    ],
    github: [
      strategy: Assent.Strategy.Github,
      mapper: KeenAuth.Mappers.Github,
      processor: MyApp.Auth.Processor,
      config: [
        client_id: System.get_env("GITHUB_CLIENT_ID"),
        client_secret: System.get_env("GITHUB_CLIENT_SECRET"),
        redirect_uri: "https://myapp.com/auth/github/callback"
      ]
    ]
  ]
```

### 2. Router Setup

```elixir
defmodule MyAppWeb.Router do
  require KeenAuth

  pipeline :auth do
    plug :fetch_session
    plug KeenAuth.Plug.FetchUser
  end

  scope "/auth" do
    pipe_through :auth
    KeenAuth.authentication_routes()
  end
end
```

### 3. Endpoint Configuration

Add the KeenAuth plug to your endpoint:

```elixir
defmodule MyAppWeb.Endpoint do
  plug KeenAuth.Plug
  plug MyAppWeb.Router
end
```

## Custom Implementation Examples

### Basic Processor

```elixir
defmodule MyApp.Auth.Processor do
  @behaviour KeenAuth.Processor

  def process(conn, provider, mapped_user, oauth_response) do
    # Simple pass-through
    {:ok, conn, mapped_user, oauth_response}
  end

  def sign_out(conn, provider, params) do
    conn
    |> KeenAuth.Storage.delete()
    |> Phoenix.Controller.redirect(to: "/")
  end
end
```

### Advanced Processor with Database Integration

```elixir
defmodule MyApp.Auth.Processor do
  @behaviour KeenAuth.Processor
  alias MyApp.{Accounts, Repo}

  def process(conn, provider, mapped_user, oauth_response) do
    with {:ok, user} <- find_or_create_user(mapped_user),
         :ok <- validate_user_permissions(user),
         {:ok, user} <- assign_user_roles(user, oauth_response) do
      {:ok, conn, user, oauth_response}
    else
      {:error, reason} ->
        conn
        |> Phoenix.Controller.put_flash(:error, "Authentication failed: #{reason}")
        |> Phoenix.Controller.redirect(to: "/login")
    end
  end

  defp find_or_create_user(mapped_user) do
    case Accounts.get_user_by_email(mapped_user.email) do
      nil -> Accounts.create_user(mapped_user)
      user -> {:ok, user}
    end
  end

  defp validate_user_permissions(user) do
    if user.active and valid_domain?(user.email) do
      :ok
    else
      {:error, "Access denied"}
    end
  end

  defp assign_user_roles(user, %{groups: groups}) do
    # Assign roles based on Azure AD groups
    roles = map_groups_to_roles(groups)
    Accounts.update_user_roles(user, roles)
  end
end
```

### Custom Mapper with Graph API Integration

```elixir
defmodule MyApp.Auth.AzureADMapper do
  @behaviour KeenAuth.Mapper
  alias MyApp.GraphAPI

  def map(provider, user_data) do
    # Start with basic normalization
    base_user = %{
      email: user_data["userPrincipalName"],
      name: user_data["displayName"],
      provider: provider
    }

    # Enrich with Graph API data
    with {:ok, token} <- get_graph_token(),
         {:ok, groups} <- GraphAPI.get_user_groups(token, base_user.email),
         {:ok, manager} <- GraphAPI.get_user_manager(token, base_user.email),
         {:ok, photo} <- GraphAPI.get_user_photo(token, base_user.email) do

      base_user
      |> Map.put(:groups, groups)
      |> Map.put(:manager, manager)
      |> Map.put(:profile_photo, photo)
    else
      _ -> base_user  # Fallback to basic data if enrichment fails
    end
  end
end
```

## Route Protection

### Require Authentication

```elixir
pipeline :authenticated do
  plug KeenAuth.Plug.RequireAuthenticated, redirect: "/login"
end

scope "/admin", MyAppWeb do
  pipe_through [:browser, :authenticated]

  get "/dashboard", AdminController, :dashboard
end
```

### Role-Based Authorization

```elixir
pipeline :admin_required do
  plug KeenAuth.Plug.RequireAuthenticated
  plug KeenAuth.Plug.Authorize.Roles, roles: ["admin", "super_admin"]
end

scope "/admin", MyAppWeb do
  pipe_through [:browser, :admin_required]

  resources "/users", UserController
end
```

## Storage Options

### Session Storage (Default)

```elixir
# No additional configuration needed
```

### Database Storage

```elixir
defmodule MyApp.Auth.DatabaseStorage do
  @behaviour KeenAuth.Storage
  alias MyApp.{Accounts, Sessions}

  def store(conn, provider, user, oauth_response) do
    with {:ok, session} <- Sessions.create_session(user, provider),
         conn <- Plug.Conn.put_session(conn, :session_id, session.id) do
      {:ok, conn}
    end
  end

  def current_user(conn) do
    with session_id when not is_nil(session_id) <- Plug.Conn.get_session(conn, :session_id),
         %{user: user} <- Sessions.get_session(session_id) do
      user
    else
      _ -> nil
    end
  end

  # Implement other required callbacks...
end
```

### JWT Token Storage

```elixir
defmodule MyApp.Auth.JWTStorage do
  @behaviour KeenAuth.Storage
  use Joken.Config

  def store(conn, provider, user, oauth_response) do
    token = generate_and_sign!(%{user_id: user.id, provider: provider})

    conn =
      conn
      |> Plug.Conn.put_resp_cookie("auth_token", token, http_only: true, secure: true)
      |> KeenAuth.assign_current_user(user)

    {:ok, conn}
  end
end
```

## Helper Functions

### Check Authentication Status

```elixir
# In your controllers or views
if KeenAuth.authenticated?(conn) do
  current_user = KeenAuth.current_user(conn)
  # User is authenticated
else
  # User is not authenticated
end
```

### Sign Out

```elixir
def sign_out(conn, _params) do
  conn
  |> KeenAuth.Storage.delete()
  |> redirect(to: "/")
end
```

## Configuration Reference

### OAuth Scopes

> [!WARNING]
> If you override `authorization_params` with custom scopes, you **must** include the essential OIDC scopes or you will receive empty user data from the provider.

KeenAuth automatically includes these default scopes when none are specified:

```
openid profile email offline_access
```

**What each scope provides:**
- `openid` - Required for OIDC, returns `sub` (user ID)
- `profile` - Returns `name`, `preferred_username`, etc.
- `email` - Returns user's email address
- `offline_access` - Returns refresh token for token renewal

**Custom scopes example:**
```elixir
# If you need additional scopes (e.g., Microsoft Graph API access),
# always include the base OIDC scopes:
config: [
  authorization_params: [
    scope: "openid profile email offline_access User.Read Directory.Read.All"
  ]
]
```

### Complete Configuration Example

```elixir
config :keen_auth,
  # Optional: Custom storage implementation
  storage: MyApp.Auth.CustomStorage,

  # Optional: Global unauthorized redirect
  unauthorized_redirect: "/login",

  # OAuth strategies
  strategies: [
    azure_ad: [
      strategy: Assent.Strategy.AzureAD,
      mapper: MyApp.Auth.AzureADMapper,
      processor: MyApp.Auth.Processor,
      config: [
        tenant_id: System.get_env("AZURE_TENANT_ID"),
        client_id: System.get_env("AZURE_CLIENT_ID"),
        client_secret: System.get_env("AZURE_CLIENT_SECRET"),
        redirect_uri: "https://myapp.com/auth/azure_ad/callback",
        scope: "openid profile email User.Read"
      ]
    ],
    google: [
      strategy: Assent.Strategy.Google,
      mapper: KeenAuth.Mappers.Default,
      processor: MyApp.Auth.Processor,
      config: [
        client_id: System.get_env("GOOGLE_CLIENT_ID"),
        client_secret: System.get_env("GOOGLE_CLIENT_SECRET"),
        redirect_uri: "https://myapp.com/auth/google/callback"
      ]
    ]
  ]
```

## Supported Providers

KeenAuth supports all OAuth providers available through the [Assent](https://github.com/pow-auth/assent) library:

- Azure Active Directory
- Google
- GitHub
- Facebook
- Twitter
- LinkedIn
- Discord
- And many more

## Development

### Running Tests

```bash
mix deps.get
mix test
```

### Code Formatting

```bash
mix format
```

### Generating Documentation

```bash
mix docs
```

## Contributing

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Support

- Documentation: [HexDocs](https://hexdocs.pm/keen_auth)
- Issues: [GitHub Issues](https://github.com/KeenMate/keen_auth/issues)
- Source: [GitHub](https://github.com/KeenMate/keen_auth)