Wrote a POC for a SAML SSO module - asking for feedback

I have written a very simple caddy saml sso module and I’d appreciate feedback from the caddy community.

I am aware we already have the amazing work from Paul but I wanted to have something simpler and more SAML specific.

Here is a Caddyfile example that instantiates the module forces valid SAML sessions on all the requests to https://foo.bar.net:12000 (/ping is open for testing).

(enable_saml) {
  saml_sso {
    saml_idp_url https://samltest.id/saml/idp
    saml_cert_file saml-cert/service.cert
    saml_key_file saml-cert/service.key
    saml_root_url https://foo.bar.net
  }
}

https://foo.bar.net:12000 {
  handle /ping {
    respond "pong"
  }

  handle /* {
    route /* {
      import enable_saml
      respond "ok"
    }
  }
}

The module is pretty simple. While provisioning, we configure the SAML library to create the Service Provider/Identity Provider connection. Then, in the middleware Handler, we either pass the requests (/saml) to the SAML library to handle the SAML flow or we ensure that all the requests are part of a valid SAML session. The SAML library provides two middleware for each purpose. If all goes well, we respond with an OK.

And feedback is welcome.

Thank you,
-drd

2 Likes

I recommend using the order global option instead of route.

You should remove /* as matchers, omitting the matcher lets Caddy use a more efficient code path, i.e. matching all requests.

1 Like

I recommend using the order global option instead of route .

I don’t think I understand the order and route directives.

Here is the Caddyfile I use:

(enable_saml) {
	saml_sso {
		saml_idp_url {$SAML_IDP_URL}
		saml_cert_file {$SAML_CERT_FILE}
		saml_key_file {$SAML_KEY_FILE}
		saml_root_url {$SAML_ROOT_URL}
	}
}

http://:12000 {
	handle /ping {
		respond "pong"
	}

	handle /* {
		route /* {
			import enable_saml
			respond "you are authenticated now"
		}
	}
}

When I do this:

handle /* {
			import enable_saml
			respond "you are authenticated now"
	}

I get an error from Caddy:

Error: adapting config using caddyfile: parsing caddyfile tokens for 'handle': directive 'saml_sso' is not an ordered HTTP handler, so it cannot be used here

Why is that? What does order mean here? To me I am already defining an order: first the saml_sso plugin and then a respond. Why do I need to add the route and/or order?

I guess this works because route forces the order?

handle /* {
		route /* {
			import enable_saml
			respond "you are authenticated now"
		}
	}

Also, how would you rewrite the Caddyfile to use order instead of route?

You should remove /* as matchers

The reason why I use matchers is because there are a few request that will hit /saml and I want to send all of them to the saml middleware but I don’t have the list of all the possible paths in the requests. If I remove the * I am only handling the / requests.

Thank you.

From the docs:

Assigns an order to HTTP handler directive(s). As HTTP handlers execute in a sequential chain, it is necessary for the handlers to be executed in the right order. Standard directives have a pre-defined order, but if using third-party HTTP handler modules, you’ll need to define the order explicitly by either using this option or placing the directive in a route block. Ordering can be described absolutely (first or last ), or relatively (before or after ) to another directive.

So, that means I have to be explicit on my handler saml_sso, the new Caddyfile reads better:

{
  order saml_sso before header
  order saml_sso before respond
}

(enable_saml) {
	saml_sso {
		saml_idp_url {$SAML_IDP_URL}
		saml_cert_file {$SAML_CERT_FILE}
		saml_key_file {$SAML_KEY_FILE}
		saml_root_url {$SAML_ROOT_URL}
	}
}

http://:12000 {
	handle /* {
    import enable_saml

    # Respond back to the user printing some of the SAML attributes
    header Content-Type text/html
    respond `<p> {http.response.header.displayname}, you are authenticated now.</p>`
	}
}

Sorry, I should have read the documentation more carefully.

1 Like

Sorry, I somehow missed your reply :frowning:

In that particular config, you don’t need handle, it doesn’t do anything for you on its own.

Also like I said, don’t do handle /*, instead just do handle if you need it. The difference is that /* is a path matcher which will always match all requests, but Caddy needs to do a path comparison to figure that out, whereas if you omit it, it avoids having to do that path comparison at all. See Request matchers (Caddyfile) — Caddy Documentation

Not a problem @francislavoie. You are legend. Thank you for helping everybody.

Ah. Perfect. More clear. It makes sense.

Ah! Now, without using a handle for / I can’t get rid of the wildcard because all the request are handled by the module. Very nice.

Final config for completion and to get the final blessing from @francislavoie

{
	order saml_sso before header
	order saml_sso before respond
}

(enable_saml) {
	saml_sso {
		saml_idp_url {$SAML_IDP_URL}
		saml_cert_file {$SAML_CERT_FILE}
		saml_key_file {$SAML_KEY_FILE}
		saml_root_url {$SAML_ROOT_URL}
	}
}

http://:12000 {
	handle /ping {
		respond "pong"
	}

  import enable_saml

  reverse_proxy / saml-app:8182 {
    header_up email {http.response.header.mail}
    header_up displayname {http.response.header.displayname}
  }
}

I don’t think you meant to use a / here. That’s a path matcher which only matches exactly / and nothing else, so this proxy will only work for requests to the root of your site and nothing else.

It doesn’t make sense to set the order for your module twice. Only the last one you use will apply. It can only have one position in the order.

Gotcha. Yes. The thing is that I have two demo Caddyfiles in my README, one where I “respond” to the client from Caddy directly and the other one (more realistic) where I forward the traffic to another service.

I think I they should both be correct now. Please confirm.

First, respond directly from Caddy:

{
  order saml_sso before header
}

(enable_saml) {
	saml_sso {
		saml_idp_url {$SAML_IDP_URL}
		saml_cert_file {$SAML_CERT_FILE}
		saml_key_file {$SAML_KEY_FILE}
		saml_root_url {$SAML_ROOT_URL}
	}
}

http://:12000 {
	handle /ping {
		respond "pong"
	}

  import enable_saml

  header Content-Type text/html
  respond `<p> {http.response.header.displayname} , you are authenticated now.</p>`
}

Second, proxy to another service:

{
	order saml_sso before header
}

(enable_saml) {
	saml_sso {
		saml_idp_url {$SAML_IDP_URL}
		saml_cert_file {$SAML_CERT_FILE}
		saml_key_file {$SAML_KEY_FILE}
		saml_root_url {$SAML_ROOT_URL}
	}
}

http://:12000 {
    handle /ping {
		respond "pong"
	}

  import enable_saml

  reverse_proxy /* saml-app:8182 {
    header_up email {http.response.header.mail}
    header_up displayname {http.response.header.displayname}
  }
}

Seems good. But again, you don’t need /*, same reasons :joy:

Can just be:

reverse_proxy saml-app:8182

:man_facepalming:

Ok, I think this is correct now:

{
	order saml_sso before header
}

(enable_saml) {
	saml_sso {
		saml_idp_url {$SAML_IDP_URL}
		saml_cert_file {$SAML_CERT_FILE}
		saml_key_file {$SAML_KEY_FILE}
		saml_root_url {$SAML_ROOT_URL}
	}
}

http://:12000 {
	handle /ping {
		respond "pong"
	}

	import enable_saml

	# Respond back to the user printing some of the SAML attributes
	header Content-Type text/html
	respond `<center>
	<p> {http.response.header.displayname} ({http.response.header.mail}), you are authenticated now.</p>
	</center>`
}

and…

{
	order saml_sso before header
}

(enable_saml) {
	saml_sso {
		saml_idp_url {$SAML_IDP_URL}
		saml_cert_file {$SAML_CERT_FILE}
		saml_key_file {$SAML_KEY_FILE}
		saml_root_url {$SAML_ROOT_URL}
	}
}

http://:12000 {
	handle /ping {
		respond "pong"
	}

	import enable_saml

	reverse_proxy saml-app:8182 {
		header_up email {http.response.header.mail}
		header_up displayname {http.response.header.displayname}
	}
}

3 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.