Skip to content

Commit caa2710

Browse files
committed
Cowboy2 migration
1 parent 3771924 commit caa2710

19 files changed

Lines changed: 242 additions & 223 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All changes are in the `main` branch (`master` remains unchanged).
44

5+
### v4.0.0
6+
7+
+ Fixed issue: #11 - Support for Cowboy 2
8+
59
### v3.6.1
610

711
+ Fixed issue: #9 - HTTP-REDIRECT wrong case

README.md

Lines changed: 10 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
An implementation of the Security Assertion Markup Language (SAML) in Erlang. So far this supports enough of the standard to act as a Service Provider (SP) to perform authentication with SAML. It has been tested extensively against the SimpleSAMLPHP IdP and can be used in production.
1+
An implementation of the Security Assertion Markup Language (SAML) in Erlang. So far this supports enough of the standard to act as a Service Provider (SP) to perform authentication with SAML. It has been tested extensively against the SimpleSAMLphp IdP and can be used in production.
22

33
### Supported protocols
44

@@ -15,98 +15,36 @@ Single log-out protocols:
1515
* SP: send LogoutRequest (REDIRECT) -> receive LogoutResponse (REDIRECT or POST)
1616
* SP: receive LogoutRequest (REDIRECT OR POST) -> send LogoutResponse (REDIRECT)
1717

18-
esaml supports RSA+SHA1/SHA256 signing of all SP payloads, and validates signatures on all IdP responses. Compatibility flags are available to disable verification where IdP implementations lack support (see the [esaml_sp record](http://arekinath.github.io/esaml/esaml.html#type-sp), and members such as `idp_signs_logout_requests`).
18+
`esaml` supports RSA+SHA1/SHA256 signing of all SP payloads, and validates signatures on all IdP responses. Compatibility flags are available to disable verification where IdP implementations lack support (see the [esaml_sp record](http://arekinath.github.io/esaml/esaml.html#type-sp), and members such as `idp_signs_logout_requests`).
1919

2020
### API documentation
2121

2222
Edoc documentation for the whole API is available at:
2323

24-
http://arekinath.github.io/esaml/
24+
https://hexdocs.pm/esaml/4.0.0/
2525

2626
### Licensing
2727

2828
2-clause BSD
2929

3030
### Getting started
3131

32-
The simplest way to use esaml in your app is with the `esaml_cowboy` module. There is an example under `examples/sp` that shows how to make a simple SAML SP in this way.
32+
The simplest way to use `esaml` in your app is with the `esaml_cowboy` module. There are two SAML Server Provider (SP) applications included in the repo under `examples` directory.
3333

34-
Point your browser to: http://127.0.0.1:8080/saml/auth
34+
The application in `examples/sp` directory shows how you can use `esaml` to enabled Single-Sign-On (SSO) in your application. This application enables an endpoint that supports Server Provider metadata request, SAML authentication request as well as the ability to consume the response from IdP.
3535

36-
Each of the protocols you wish to support will normally require at least one distinct URL endpoint, plus one additional URL for the SAML SP metadata. In the `sp` example, only one protocol is used: the single-sign-on SP AuthnRequest -> Response + Assertion protocol.
37-
38-
The typical approach is to use a single Cowboy route for all SAML endpoints:
39-
40-
```erlang
41-
Dispatch = cowboy_router:compile([
42-
{'_', [
43-
{"/saml/:operation", sp_handler, []}
44-
]}
45-
])
46-
```
47-
48-
Then, based on the value of the `operation` binding, you can decide which protocol to proceed with, by matching these up with the URIs you supply to `esaml_sp:setup/1`.
49-
50-
```erlang
51-
init(_Transport, Req, _Args) ->
52-
...
53-
SP = esaml_sp:setup(#esaml_sp{
54-
consume_uri = Base ++ "/consume",
55-
metadata_uri = Base ++ "/metadata",
56-
...
57-
}),
58-
...
59-
60-
handle(Req, S = #state{}) ->
61-
{Operation, Req2} = cowboy_req:binding(operation, Req),
62-
{Method, Req3} = cowboy_req:method(Req2),
63-
handle(Method, Operation, Req3, S).
64-
65-
handle(<<"GET">>, <<"metadata">>, Req, S) ->
66-
...
67-
68-
handle(<<"POST">>, <<"consume">>, Req, S) ->
69-
...
70-
```
71-
72-
The functions on the `esaml_cowboy` module can either parse and validate an incoming SAML payload, or generate one and reply to the request with it.
73-
74-
For example, the way the metadata endpoint is handled in the example is to unconditionally call `esaml_cowboy:reply_with_metadata/2`, which generates the SP metadata and replies to the request:
75-
76-
```erlang
77-
handle(<<"GET">>, <<"metadata">>, Req, S = #state{sp = SP}) ->
78-
{ok, Req2} = esaml_cowboy:reply_with_metadata(SP, Req),
79-
{ok, Req2, S};
80-
```
81-
82-
On the other hand, the consumer endpoint (which handles the second step in the SSO protocol, receiving the Response + Assertion from the IdP) has to validate its payload before replying:
83-
84-
```erlang
85-
handle(<<"POST">>, <<"consume">>, Req, S = #state{sp = SP}) ->
86-
case esaml_cowboy:validate_assertion(SP, Req) of
87-
{ok, Assertion, RelayState, Req2} ->
88-
% authentication success!
89-
...;
90-
91-
{error, Reason, Req2} ->
92-
{ok, Req3} = cowboy_req:reply(403, [{<<"content-type">>, <<"text/plain">>}],
93-
["Access denied, assertion failed validation\n"], Req2),
94-
{ok, Req3, S}
95-
end;
96-
```
97-
98-
More complex configurations, including multiple IdPs, dynamic retrieval of IdP metadata, and integration with many kinds of application authentication systems are possible.
99-
100-
The second esaml example, `sp_with_logout` demonstrates the addition endpoints necessary to enable Single Log-out protocol support. It also shows how you can build a bridge from esaml to local application session storage, by generating session cookies for each user that logs in (and storing them in ETS).
36+
The second application in `example/sp_with_logout` shows how Single Logout can be enabled. It also shows how you can build a bridge from `esaml` to local application session storage, by generating session cookies for each user that logs in (and storing them in ETS).
10137

10238
### More advanced usage
10339

104-
You can also tap straight into lower-level APIs in esaml if `esaml_cowboy` doesn't meet your needs. The `esaml_binding` and `esaml_sp` modules are the interface used by `esaml_cowboy` itself, and contain all the basic primitives to generate and parse SAML payloads.
40+
You can also tap straight into lower-level APIs in `esaml` if `esaml_cowboy` doesn't meet your needs. The `esaml_binding` and `esaml_sp` modules are the interface used by `esaml_cowboy` itself, and contain all the basic primitives to generate and parse SAML payloads.
10541

10642
This is particularly useful if you want to implement SOAP endpoints using SAML.
10743

44+
> The Elixir library `Samly` is one such implementation. It dose not use `esaml_cowboy`. Instead it relies on the lower-level APIs and uses Elixir `Plug` and `Cowboy` directly for endpoints/routing.
45+
10846
### Contributions
10947

110-
Pull requests are always welcome for bug fixes and improvements. Fixes that enable compatibility with different IdP implementations are usually welcome, but please ensure they do not come at the expense of compatibility with another IdP. esaml prefers to follow as closely to the SAML standards as possible.
48+
Pull requests are always welcome for bug fixes and improvements. Fixes that enable compatibility with different IdP implementations are usually welcome, but please ensure they do not come at the expense of compatibility with another IdP. `esaml` prefers to follow as closely to the SAML standards as possible.
11149

11250
Bugs/issues opened without patches are also welcome, but might take a lot longer to be looked at. ;)

examples/sp/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.swp
22
deps
3+
_checkouts

examples/sp/rebar.config

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{deps, [
2-
{esaml, ".*", {git, "https://github.com/K2InformaticsGmbH/esaml", "HEAD"}},
3-
{cowboy, "1.0.3", {git, "https://github.com/K2InformaticsGmbH/cowboy", {tag, "1.0.3"}}}
2+
{esaml, "4.0.0"},
3+
{cowboy, "2.6.0"}
44
]}.

examples/sp/src/sp.app.src

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{application, sp,
22
[
3-
{description, "SAML for erlang"},
4-
{vsn, "1"},
3+
{description, "SAML SP for erlang"},
4+
{vsn, "2"},
55
{registered, []},
66
{included_applications, [
77
]},

examples/sp/src/sp_app.erl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
-export([stop/1]).
1515

1616
start(_Type, _Args) ->
17+
HostMatch = '_',
18+
PathMatch = "/saml/:operation",
19+
InitialState = #{},
1720
Dispatch = cowboy_router:compile([
18-
{'_', [
19-
{"/saml/:operation", sp_handler, []}
20-
]}
21-
]),
22-
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
23-
{env, [{dispatch, Dispatch}]}
21+
{HostMatch, [{PathMatch, sp_handler, InitialState}]}
2422
]),
23+
{ok, _} = cowboy:start_clear(sp_http_listener, [{port, 8080}],
24+
#{env => #{dispatch => Dispatch}}),
2525
sp_sup:start_link().
2626

2727
stop(_State) ->

examples/sp/src/sp_handler.erl

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
-module(sp_handler).
1010
-include_lib("esaml/include/esaml.hrl").
1111

12-
-record(state, {sp, idp}).
13-
-export([init/3, handle/2, terminate/3]).
12+
-export([init/2, terminate/3]).
1413

15-
init(_Transport, Req, _Args) ->
14+
init(Req, State = #{initialized := true}) ->
15+
Operation = cowboy_req:binding(operation, Req),
16+
Method = cowboy_req:method(Req),
17+
io:format("[Method] ~p~n", [Method]),
18+
io:format("[Operation] ~p~n", [Operation]),
19+
io:format("[State] ~p~n", [State]),
20+
handle(Method, Operation, Req, State);
21+
22+
init(Req, State) ->
1623
% Load the certificate and private key for the SP
1724
PrivKey = esaml_util:load_private_key("priv/test.key"),
1825
Cert = esaml_util:load_certificate("priv/test.crt"),
@@ -41,44 +48,40 @@ init(_Transport, Req, _Args) ->
4148
% (this call will cache after the first time around, so it will be fast)
4249
IdpMeta = esaml_util:load_metadata("https://some.idp.com/idp/saml2/idp/metadata.php"),
4350

44-
{ok, Req, #state{sp = SP, idp = IdpMeta}}.
45-
46-
handle(Req, S = #state{}) ->
47-
{Operation, Req2} = cowboy_req:binding(operation, Req),
48-
{Method, Req3} = cowboy_req:method(Req2),
49-
handle(Method, Operation, Req3, S).
51+
State1 = State#{sp => SP, idp => IdpMeta, initialized => true},
52+
init(Req, State1).
5053

5154
% Return our SP metadata as signed XML
52-
handle(<<"GET">>, <<"metadata">>, Req, S = #state{sp = SP}) ->
53-
{ok, Req2} = esaml_cowboy:reply_with_metadata(SP, Req),
54-
{ok, Req2, S};
55+
handle(<<"GET">>, <<"metadata">>, Req, State = #{sp := SP}) ->
56+
Req2 = esaml_cowboy:reply_with_metadata(SP, Req),
57+
{ok, Req2, State};
5558

5659
% Visit /saml/auth to start the authentication process -- we will make an AuthnRequest
5760
% and send it to our IDP
58-
handle(<<"GET">>, <<"auth">>, Req, S = #state{sp = SP,
59-
idp = #esaml_idp_metadata{login_location = IDP}}) ->
60-
{ok, Req2} = esaml_cowboy:reply_with_authnreq(SP, IDP, <<"foo">>, Req),
61-
{ok, Req2, S};
61+
handle(<<"GET">>, <<"auth">>, Req, State = #{sp := SP,
62+
idp := #esaml_idp_metadata{login_location = IDP}}) ->
63+
Req2 = esaml_cowboy:reply_with_authnreq(SP, IDP, <<"foo">>, Req),
64+
{ok, Req2, State};
6265

6366
% Handles HTTP-POST bound assertions coming back from the IDP.
64-
handle(<<"POST">>, <<"consume">>, Req, S = #state{sp = SP}) ->
67+
handle(<<"POST">>, <<"consume">>, Req, State = #{sp := SP}) ->
6568
case esaml_cowboy:validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Req) of
6669
{ok, Assertion, RelayState, Req2} ->
6770
Attrs = Assertion#esaml_assertion.attributes,
6871
Uid = proplists:get_value(uid, Attrs),
6972
Output = io_lib:format("<html><head><title>SAML SP demo</title></head><body><h1>Hi there!</h1><p>This is the <code>esaml_sp_default</code> demo SP callback module from eSAML.</p><table><tr><td>Your name:</td><td>\n~p\n</td></tr><tr><td>Your UID:</td><td>\n~p\n</td></tr></table><hr /><p>RelayState:</p><pre>\n~p\n</pre><p>The assertion I got was:</p><pre>\n~p\n</pre></body></html>", [Assertion#esaml_assertion.subject#esaml_subject.name, Uid, RelayState, Assertion]),
70-
{ok, Req3} = cowboy_req:reply(200, [{<<"Content-Type">>, <<"text/html">>}], Output, Req2),
71-
{ok, Req3, S};
73+
Req3 = cowboy_req:reply(200, #{<<"Content-Type">> => <<"text/html">>}, Output, Req2),
74+
{ok, Req3, State};
7275

7376
{error, Reason, Req2} ->
74-
{ok, Req3} = cowboy_req:reply(403, [{<<"content-type">>, <<"text/plain">>}],
77+
Req3 = cowboy_req:reply(403, #{<<"content-type">> => <<"text/plain">>},
7578
["Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason])],
7679
Req2),
77-
{ok, Req3, S}
80+
{ok, Req3, State}
7881
end;
7982

80-
handle(_, _, Req, S = #state{}) ->
81-
{ok, Req2} = cowboy_req:reply(404, [], <<"Not found">>, Req),
82-
{ok, Req2, S}.
83+
handle(_, _, Req, State = #{}) ->
84+
Req2 = cowboy_req:reply(404, #{}, <<"Not found">>, Req),
85+
{ok, Req2, State}.
8386

8487
terminate(_Reason, _Req, _State) -> ok.

examples/sp/start.sh

100644100755
Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
11
#!/bin/bash
22

3-
unamestr=`uname`
4-
host=127.0.0.1
5-
name=sp@$host
6-
if [[ "$unamestr" == 'Linux' || "$unamestr" == 'Darwin' ]]; then
7-
exename=erl
3+
HOST="${host:=127.0.0.1}"
4+
APP_NAME="esaml_example_sp"
5+
COOKIE="${APP_NAME}"
6+
NODE_NAME="${APP_NAME}@${HOST}"
7+
UNAME_STR=`uname`
8+
if [[ "${UNAME_STR}" == 'Linux' || "${UNAME_STR}" == 'Darwin' ]]; then
9+
EXE_NAME=erl
810
else
9-
exename='start //MAX werl.exe'
11+
EXE_NAME='start //MAX werl.exe'
1012
#exename='erl.exe'
1113
fi
1214

1315
# Node name
14-
node_name="-name $name"
16+
NODE_NAME_OPT="-name ${NODE_NAME}"
1517

1618
# Cookie
17-
cookie="-setcookie $ck"
19+
COOKIE_OPT="-setcookie ${COOKIE}"
1820

1921
# PATHS
20-
paths="-pa"
21-
paths=$paths" ebin"
22-
paths=$paths" deps/*/ebin"
22+
PATHS_OPT="-pa"
23+
PATHS_OPT="${PATHS_OPT} _build/default/lib/*/ebin"
24+
PATHS_OPT="${PATHS_OPT} _checkouts/*/ebin"
2325

24-
start_opts="$paths $cookie $node_name"
26+
START_OPTS="${PATHS_OPT} ${COOKIE_OPT} ${NODE_NAME_OPT}"
2527

2628
# DDERL start options
2729
echo "------------------------------------------"
28-
echo "Starting ESaml (SP)"
30+
echo "Starting ESaml Example (SP)"
2931
echo "------------------------------------------"
30-
echo "Node Name : $node_name"
31-
echo "Cookie : $cookie"
32-
echo "EBIN Path : $paths"
32+
echo "Node Name : ${NODE_NAME}"
33+
echo "Cookie : ${COOKIE}"
34+
echo "EBIN Path : ${PATHS_OPT}"
3335
echo "------------------------------------------"
3436

3537
# Starting dderl
36-
$exename $start_opts -eval "application:ensure_all_started(sp)."
38+
${EXE_NAME} ${START_OPTS} -eval "application:ensure_all_started(sp)."

examples/sp_with_logout/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_checkouts
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIC/TCCAeWgAwIBAgIJAOrjLRGkHGpYMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
3+
BAMMCWxvY2FsaG9zdDAgFw0xNjA0MjYwNzU4MDVaGA8yMTE2MDQwMjA3NTgwNVow
4+
FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
5+
CgKCAQEAzxk42NUZyenAl3JComTM03Z/MOzO8os5BZDjSOcK9JGSpCUzlFU/hGxG
6+
ZNCuykrX6QhCcBAdu9cM/BT+AO3QCoXyEKQHdMpu+EisAhTBJFu0mTOeeoATLJEa
7+
CrUL7Wgf/8UB2HQd+43S0HQnGDqSHBE5oHmzJmiTQ0pd1VWTvCOx/VYvBjkrnzfe
8+
0C8DZMiZ6cNDMcnS/M10YvuD9/PZ0w9202mImfB2lVPYaMPYd3qjU++nuwpYHQUz
9+
MO8enJOVgMtRvV6xTlAV2Cu2yAXQjJWZw8ZA7AarF2GrjGicGcGCvnm8FTpHEqL7
10+
Wii0FzT6fOMPZmRw00sErWhQ5PxrgQIDAQABo1AwTjAdBgNVHQ4EFgQUTzIbssmF
11+
/pGgV7+D1A7kf+6Wn7swHwYDVR0jBBgwFoAUTzIbssmF/pGgV7+D1A7kf+6Wn7sw
12+
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAzZy2R85R1pJ6dwgt12Yo
13+
3iaXuH2D5jIzhFDDIdeUakrMOMKUq4At8OFToNl/F2WtZiBDyoCX4t+UVzybJl1A
14+
8CRsw0lz/ayF6UjAbFlLxESFKRa6tgkhrcBc9gFq/tO8bLRGT+ZKGoysFMu2lgtS
15+
LjZbr1lknxql7yXRjtAd5Tcz6GYGfOHJeVXkJMtueZUgmWkKUc6h7C6SP0scfLMG
16+
a4ucPXkQWi/QyF8Bziq1owR0aRMQyBO97Ua9eARYKCqReUP1WRMM4KJ0DKW0du/v
17+
l7f7o63DOOIp5cO5OnGUCsRfnGk7/NgvPpe0k1wE/alJTx9vfQfk2MLXWrhfO7ZJ
18+
rA==
19+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)