goauthentik / authentik

The authentication glue you need.
https://goauthentik.io
Other
13.88k stars 942 forks source link

Support GET requests on /application/o/token #11714

Open archont94 opened 1 month ago

archont94 commented 1 month ago

Is your feature request related to a problem? Please describe. I want to use authentik as oauth2 for docker registry (distribution/distribution on GitHub). Unfortunately it uses GET requests for /application/o/token endpoint instead of POST (see https://github.com/distribution/distribution/blob/main/docs/content/spec/auth/token.md for details, TLDR: To respond to this challenge, the client will need to make a GET request to the URL).

Describe the solution you'd like It would be great if authentik could support both POST and GET requests on this endpoint.

Additional context I'm not sure why they decided to use GET and if there is any RFC which covers which version should be used, but I assume that adding support for both in authentik wouldn't be problematic.

This is the request docker client does to authentik, in order to obtain token: "GET /application/o/token?account=USERNAME&client_id=docker&offline_token=true&service=SERVICE_VALUE_FROM_REGISTRY_CONFIG HTTP/1.1" 400 140 "-" "docker/24.0.7 go/go1.20.10 git-commit/311b9ff kernel/5.15.153.1-microsoft-standard-WSL2 os/linux arch/amd64 UpstreamClient(Docker-Client/24.0.7 \x5C(linux\x5C))". Client ID seems to be hardcoded, but it can be set to any value in authentik so that should work. SERVICE_VALUE_FROM_REGISTRY_CONFIG is value from config.yml registry file, section auth.token.service.

rissson commented 1 month ago

The RFC specifies this endpoint as a POST https://datatracker.ietf.org/doc/html/rfc6749#section-6. Seems like distribution/distribution is not following the spec

archont94 commented 1 month ago

Is there any chance that authentik will support this custom docker implementation (similar to how GitHub is supported: https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/#github-compatibility)?

I have this simple python proxy to translate GET requests to POST ones and move params from url query to body, but it isn't great (some images aren't loading on authentik site etc). If this would be part of authentik (similar to github custom compability layer), this issue would be solved. Its not even close to being safe, I just wanted quickly confirm that docker client can actually use authentik oauth. There should be a lot of extra safe checks etc.

import aiohttp
import base64
from aiohttp import web
import json

UPSTREAM_URL = "http://authentik-server:9000"  # docker service name

async def handle(request):
    request_method = request.method
    request_url = f"{UPSTREAM_URL}{request.path}"
    data = await request.read()
    params = request.query
    headers = request.headers

    docker_token_query = False
    if "/application/o/token" in request.path and request_method == "GET":
        docker_token_query = True
        request_url = "https://authentik.example.com/application/o/token/" # to avoid wrong `iss` value in token we can use external API address, as this will be POST anyway (so its not recurrence)
        data = aiohttp.FormData()

        data.add_field('grant_type', 'client_credentials') # value expected by authentik

        client_id = params.get('service') # query param 'client_id' is hardcoded to 'docker' by client, but we can set 'service' field on docker registry config to any value
        data.add_field('client_id', client_id)

        auth_header = headers.get('Authorization')
        if auth_header is None:
            raise web.HTTPUnauthorized()
        base64_credentials = auth_header.split(' ')[1] # TODO - its unsafe, there should be checks if this is Basic auth etc
        decoded_credentials = base64.b64decode(base64_credentials).decode('utf-8')
        username, password = decoded_credentials.split(':', 1)
        data.add_field('username', username)
        data.add_field('password', password)

        request_method = "POST"
        params = None
        headers = None

    async with aiohttp.ClientSession() as session:
        async with session.request(request_method, request_url,
                                   data=data,
                                   params=params,
                                   headers=headers,
                                   max_redirects=50) as resp:
            resp_body = await resp.read()
            if docker_token_query:
                if resp.status == 200:
                    resp_body = json.loads(resp_body.decode('utf-8'))
                    resp_body['token'] = resp_body['access_token'] # compability with older docker clients
                    resp_body = json.dumps(resp_body).encode('utf-8')
                print(f"response status {resp.status}, body {resp_body}")
            return web.Response(status=resp.status, body=resp_body, content_type=resp.content_type)

async def main():
    app = web.Application()
    app.router.add_route('*', '/{tail:.*}', handle)
    return app

if __name__ == '__main__':
    web.run_app(main(), port=8080)

Example config.yml for docker registry:

version: 0.1

log:
  level: debug
  fields:
    service: registry

storage:
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /var/lib/registry

http:
  addr: :5000

auth:
  token:
    realm: https://authentik.example.com/application/o/token/
    service: CLIENT_ID_FROM_AUTHENTIK
    issuer: http://authentik.example.com/application/o/SLUG_NAME/ # OpenID Configuration Issuer from Authentik
    rootcertbundle: /certs/token.crt # optional, same as one used by authentik to sign oauth2
    jwks: /certs/jwks.json # value obtained from https://authentik.example.com/application/o/docker-dev-registry/jwks/ saved as json on disk

health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3