authentik icon indicating copy to clipboard operation
authentik copied to clipboard

Implement Webfinger (RFC 7033)

Open fuomag9 opened this issue 1 year ago • 8 comments

Is your feature request related to a problem? Please describe. Currently it's not possible to implement a custom provider for tailscale due to the requirement of having the web finger endpoint https://tailscale.com/kb/1240/sso-custom-oidc/ at https://${domain}/.well-known/webfinger

Describe the solution you'd like I'd like for the Webfinger endpoint to be implemented as per rfc7033

Describe alternatives you've considered None, as the endpoint is needed to configure tailscale

Additional context None

fuomag9 avatar May 01 '23 16:05 fuomag9

Hello everyone,

Thank you for raising this as an issue, I am unsure how helpful this is for you but I had issues setting up Tailscale so I have made a bodge attempt to work around this and it worked

The Challenge: You may be aware of the requirement for a WebFinger endpoint to setup TailScale SSO(https://tailscale.com/kb/1240/sso-custom-oidc/) at the URL https://${domain}/.well-known/webfinger and this is lacking in the Authentik's SSO integration.

Implemented Integration: To surmount this challenge, I've implemented a custom WebFinger endpoint, trying to be adhering to the RFC7033 specification. This integration ensures adherence to the required standard and seamless compatibility with Authentik's SSO features.

Here's a snippet of the code illustrating the integration, which leverages Python's http.server module: I am unsure if their are any security considerations with this but since Tailscale in my example didn't require it to stay online I removed it once OIDC was configured


from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import json

class WebFingerHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith('/.well-known/webfinger'):
            parsed_url = urlparse(self.path)
            query_params = parse_qs(parsed_url.query)
            
            if 'resource' in query_params:
                resource = query_params['resource'][0]
                
                if resource.startswith('acct:'):
                    email = resource[5:]
                    issuer_url = "https://idp.example.com/application/o/tailscale/"
                    response_data = {
                        "subject": resource,
                        "links": [
                            {
                                "rel": "http://openid.net/specs/connect/1.0/issuer",
                                "href": issuer_url
                            },
                            {
                                "rel": "authorization_endpoint",
                                "href": issuer_url + "oauth2/authorize"
                            },
                            {
                                "rel": "token_endpoint",
                                "href": issuer_url + "oauth2/token"
                            },
                            {
                                "rel": "userinfo_endpoint",
                                "href": issuer_url + "userinfo"
                            },
                            {
                                "rel": "jwks_uri",
                                "href": issuer_url + "jwks"
                            }
                        ]
                    }
                    self.send_response(200)
                    self.send_header("Content-type", "application/json")
                    self.end_headers()
                    self.wfile.write(json.dumps(response_data).encode())
                    return
            
        self.send_response(404)
        self.end_headers()
        self.wfile.write(b"Resource not found")

def run_server(server_class=HTTPServer, handler_class=WebFingerHandler, port=8000):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print(f"Starting WebFinger server on port {port}")
    httpd.serve_forever()

if __name__ == '__main__':
    run_server()



Acknowledging Solution Scope:

Before we proceed further, I want to acknowledge that while this workaround effectively solves the challenge some face, it may not be universally applicable to all systems and use cases. However, based on my personal experience, this solution has proven to be effective.

Request for Official Integration: I'm hope soon for the possibility of integrating this WebFinger solution directly into Authentik's SSO capabilities.

Your insights, feedback, and suggestions on my workaround would be invaluable as I am new to SSO/IDP development Kind Regards Billy-George

billyprice1 avatar Aug 17 '23 19:08 billyprice1

This would also require some way to select which Provider should be used for the issuer URL

septatrix avatar Dec 04 '23 16:12 septatrix

We'll probably add this once we add full tenancy support and allow configuring this on a per tenant setting

BeryJu avatar Dec 04 '23 18:12 BeryJu

Hi, Just tagging it here for reference: #7590

nab-os avatar Dec 29 '23 04:12 nab-os

We'll probably add this once we add full tenancy support and allow configuring this on a per tenant setting

I wonder if it would be better as a per-brand setting. (Note: current tenants are being renamed into brands). That way a single tenant can still have multiple webfinger endpoints

rissson avatar Jan 03 '24 11:01 rissson

Hi - I'm just curious about the status of this as Authentik is listed on Tailscale as an official Integration.

Edit: Never mind, I tried Tailscale's WebFinger Tool and pointed it to my Authentik instance and it came up with a 404.

@billyprice1 - where did you place that file?

ajtatum avatar Mar 29 '24 16:03 ajtatum

@ajtatum

@billyprice1 - where did you place that file?

The snippet that @billyprice1 graciously provided is a simple python http server with one endpoint - /.well-known/webfinger. It will not work by just placing a file if all you have is a server. It has to be run, with python, in background.

To serve this endpoint it would be probably be easiest to use a reverse proxy like nginx or Caddy in addition to running this python server in background on your server.

A sample Caddyfile (please keep in mind this should be modified to server other traffic):

:443 {
    handle /.well-known/webfinger {
         #This should point to the port that the Python script is running on, default is 8000
         reverse_proxy localhost:8000    
    }
    
    # Other reverse proxies go here
    handle {
        reverse_proxy other_backend_servers
    }
}

You can run every part of this in docker or use systemd to have this all run in the background.

If you have nothing else on the domain you want to server the webfinger at then you might as well run the python script in the background:

python3 ./path/to/the/script

zmilonas avatar Apr 11 '24 10:04 zmilonas