Badgerati / Pode

Pode is a Cross-Platform PowerShell web framework for creating REST APIs, Web Sites, and TCP/SMTP servers
https://badgerati.github.io/Pode
MIT License
842 stars 91 forks source link

OAuth2 with Reverse Proxy infront #906

Open RobinBeismann opened 2 years ago

RobinBeismann commented 2 years ago

Question

I'm trying to create a Pode Service that shall serve as Forward Auth Proxy for the docker based reverse proxy called Traefik2.

Based on the docs I got that I need to set a redirect URL if running behind a reverse proxy otherwise Pode will deliver the local IP address which will (correctly) be rejected as redirect URL by Azure AD.

So I went ahead and configured the parameter -RedirectUrl "https://auth.demo.system32.blog/oauth2/callback" to have Pode sent this one as redirect to Azure AD. In Azure AD I set the redirect URL "https://auth.demo.system32.blog/oauth2/callback" as reflected on the Pode Configuration.

However as soon as I open up the webpage, I get forwarded to Azure AD, authenticated and back to Pode where I receive a 404 for "http://auth.demo.system32.blog/oauth2/callback" It is http:// and no longer https:// but I assume this is normal due to the SSL offloading at Traefik2.

Traefik is correctly forwarding the X-Forwarded-*. For reference, here is a container served with the same configuration that just dumps the HTTP Variables: https://whoami.demo.system32.blog/

Am I missing something? To me it looks like Pode stops listening on /oauth2/callback as soon as I specify it as redirect url, but I can't really figure out why after checking the code that is used. I even tested modifying "Get-PodeOAuth2RedirectHost" to always apply the workaround for IIS/Heroku as it seems to behave the same using the headers.

My server.ps1 looks like this:

Import-Module /usr/local/share/powershell/Modules/Pode/Pode.psm1 -Force -ErrorAction Stop

Start-PodeServer {
    # http endpoint
    Add-PodeEndpoint -Address * -Port 80 -Protocol Http

    # Logging to console
    New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
    New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging

    # Session Middleware
    Enable-PodeSessionMiddleware -Duration 120 -Extend

    # Generate Azure AD Scheme
    $scheme = New-PodeAuthAzureADScheme -ClientID '' -ClientSecret '' -Tenant '' -RedirectUrl "https://auth.demo.system32.blog/oauth2/callback"

    $scheme | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock {
        param($user, $accessToken, $refreshToken, $response)

        # check if the user is valid

        return @{ User = $user }
    }

    # home page:
    # redirects to login page if not authenticated
    Add-PodeRoute -Method Get -Path '/' -Authentication Login -ScriptBlock {
        Write-PodeViewResponse -Path 'home' -Data @{ Username = $WebEvent.Auth.User.name }
    }

    # login - this will just redirect to azure
    # NOTE: you do not need the -Login switch
    Add-PodeRoute -Method Get -Path '/login' -Authentication Login

    # logout
    Add-PodeRoute -Method Post -Path '/logout' -Authentication Login -Logout
}

My docker-compose file looks like this and basically maps a local directory into the container while starting the actual Pode instance using a start-at-boot command:

version: '2'

services:
  podeauth:
    restart: always
    image: badgerati/pode:latest-alpine
    hostname: pode
    volumes:
      - ./pode:/usr/src/app/

    labels:
      # Traefik General
      - 'traefik.enable=true'
      - "traefik.docker.network=gateway"

      # Pode itself
      - 'traefik.http.routers.PodeAuth.rule=Host(`auth.demo.system32.blog`)'
      - 'traefik.http.routers.PodeAuth.entrypoints=websecure'
      - "traefik.http.routers.PodeAuth.service=PodeAuthService"
      - "traefik.http.services.PodeAuthService.loadBalancer.server.port=80"

      # Use Letsencrypt
      - "traefik.http.routers.PodeAuth.tls.certresolver=le_production"

    command: pwsh -c "cd /usr/src/app; ./server.ps1"
RobinBeismann commented 2 years ago

Ah, if I set -Hostname "auth.demo.system32.blog" and omit the -RedirectUrl Parameter it works.

Would it make sense to somehow honor the X-Forwarded-Host Header for this? Like if it is set, use it to build the redirect url? I'd submit a pull request if you'd find it useful.

Badgerati commented 2 years ago

Hi @RobinBeismann,

I just went through the docs to check, and it does "kinda" mention why this happens, but it's definitely worded badly πŸ™ˆ

When a -RedirectUrl is supplied, Pode doesn't create a route itself - it needs to be created by yourself. Like if you wanted it as /my-auth/awesome/callback, you'd also need to do:

Add-PodeRoute -Method Get -Path '/my-auth/awesome/callback'

Same applies for if you need a custom https://<hostname>/oauth2/callback. I could make Pode get the URL path and check if a route exists, and create one then πŸ€”

Looking at Get-PodeOAuth2RedirectHost you're probably right actually. If an X-Forwarded-* header exists just use that - then it works for any host/proxy:

if ($RedirectUrl.StartsWith('/')) {
    if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku -or (Test-PodeHeader -Name 'X-Forwarded-Proto')) {
        $protocol = Get-PodeHeader -Name 'X-Forwarded-Proto'
        if ([string]::IsNullOrWhiteSpace($protocol)) {
            $protocol = 'https'
        }

        $domain = "$($protocol)://$($WebEvent.Request.Host)"
    }
    else {
        $domain = Get-PodeEndpointUrl
    }

    $RedirectUrl = "$($domain.TrimEnd('/'))$($RedirectUrl)"
}

perhaps? πŸ€”

RobinBeismann commented 2 years ago

I'll create a pull request tomorrow. πŸ‘

I found another thing that I'd need to use it as Forward Auth Proxy for another application, which is the ability to set a custom domain for the Session Middleware Cookie.

On my test, the Pode Auth proxy was auth.demo.system32.blog, and Traefik2 is forwarding the initial request to Pode inheriting the 302 Found to Azure AD and resulting in Pode issuing a cookie for testapp.demo.system32.blog. After that Azure AD authenticates and forwards back to Pode which then issues a new cookie for auth.demo.system32.blog and the OAuth2 challenge to fail.

When I manually overwrite the domain during the cookie generation in Pode to a domain that holds both (demo.system32.blog in this case) the cookie fits for both and it doesn't generate a second one so the OAuth2 challenge works.

I'll implement that too before I raise the pull request.