├── rebar ├── .gitignore ├── Makefile ├── src ├── ejwt.app.src └── ejwt.erl ├── rebar.config ├── LICENSE └── README.md /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kato-im/ejwt/HEAD/rebar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: deps compile 2 | 3 | compile: 4 | ./rebar compile 5 | 6 | deps: 7 | ./rebar get-deps 8 | 9 | clean: 10 | ./rebar clean 11 | rm -fr ebin 12 | -------------------------------------------------------------------------------- /src/ejwt.app.src: -------------------------------------------------------------------------------- 1 | {application, ejwt, 2 | [ 3 | {description, ""}, 4 | {vsn, git}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | crypto, 10 | jiffy, 11 | base64url, 12 | ej 13 | ]}, 14 | {env, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [warnings_as_errors]}. 2 | {deps, [ 3 | {jiffy, ".*", {git, "git://github.com/davisp/jiffy", 4 | {tag, "0.14.8"}}}, 5 | {base64url, ".*", {git, "git://github.com/dvv/base64url.git", 6 | {tag, "v1.0"}}}, 7 | {ej, ".*", {git, "git://github.com/seth/ej.git", 8 | {tag, "0.0.3"}}} 9 | ]}. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Katō 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Erlang JWT Library 2 | = 3 | 4 | JWT is a simple authorization token [format](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) based on JSON. [Peter Hizalev](http://twitter.com/petrohi) wrote this library at [Kato.im](http://kato.im). We're open-sourcing it in case someone else needs to create or parse JWT tokens with Erlang. 5 | 6 | ## Smoke test example 7 | 8 | Install 9 | 10 | git clone git@github.com:kato-im/ejwt.git && cd ejwt 11 | ./rebar get-deps 12 | erl 13 | 14 | In Erlang shell: 15 | 16 | %% Create JWT token 17 | application:start(crypto). 18 | Key = <<"53F61451CAD6231FDCF6859C6D5B88C1EBD5DC38B9F7EBD990FADD4EB8EB9063">>. 19 | Claims = {[ 20 | {user_id, <<"bob123">>}, 21 | {user_name, <<"Bob">>} 22 | ]}. 23 | ExpirationSeconds = 86400, 24 | Token = ejwt:jwt(<<"HS256">>, Claims, ExpirationSeconds, Key). 25 | 26 | %% Parse JWT token 27 | ejwt:parse_jwt(Token, Key). 28 | 29 | 30 | You should get back the original claims Jterm, plus expiration claim: 31 | 32 | {[ 33 | {<<"exp">>,1392607527}, 34 | {<<"user_id">>,<<"bob123">>}, 35 | {<<"user_name">>,<<"Bob">>} 36 | ]} 37 | 38 | -------------------------------------------------------------------------------- /src/ejwt.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% JWT Library for Erlang. 3 | %% Written by Peter Hizalev at Kato (http://kato.im) 4 | %% 5 | 6 | -module(ejwt). 7 | 8 | -export([pre_parse_jwt/1]). 9 | -export([parse_jwt/2]). 10 | -export([parse_jwt_iss_sub/2]). 11 | -export([jwt/3, jwt/4]). 12 | -export([jwt_hs256_iss_sub/4]). 13 | 14 | jiffy_decode_safe(Bin) -> 15 | R = try jiffy:decode(Bin) of Jterm0 -> Jterm0 catch Err -> Err end, 16 | case R of 17 | {error, _} -> 18 | invalid; 19 | {List} = Jterm -> 20 | %% force absence of duplicate keys http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#Claims 21 | Keys = [K || {K, _} <- List], 22 | case length(lists:usort(Keys)) =:= length(Keys) of 23 | true -> 24 | Jterm; 25 | false -> 26 | invalid 27 | end; 28 | _ -> 29 | invalid 30 | end. 31 | 32 | pre_parse_jwt(Token) -> 33 | case decode_jwt(split_jwt_token(Token)) of 34 | {_HeaderJterm, ClaimSetJterm, _Signature} -> 35 | ClaimSetJterm; 36 | invalid -> 37 | invalid 38 | end. 39 | 40 | parse_jwt(Token, Key) -> 41 | SplitToken = split_jwt_token(Token), 42 | case decode_jwt(SplitToken) of 43 | {HeaderJterm, ClaimSetJterm, Signature} -> 44 | [Header, ClaimSet | _] = SplitToken, 45 | Type = ej:get({<<"typ">>}, HeaderJterm), 46 | Alg = ej:get({<<"alg">>}, HeaderJterm), 47 | case parse_jwt_check_sig(Type, Alg, Header, ClaimSet, Signature, Key) of 48 | false -> invalid; 49 | true -> 50 | case parse_jwt_has_expired(ClaimSetJterm) of 51 | true -> expired; 52 | false -> ClaimSetJterm 53 | end 54 | end; 55 | invalid -> invalid 56 | end. 57 | 58 | parse_jwt_has_expired(ClaimSetJterm) -> 59 | Expiry = ej:get({<<"exp">>}, ClaimSetJterm, none), 60 | case Expiry of 61 | none -> 62 | false; 63 | _ -> 64 | case (ej:get({<<"exp">>}, ClaimSetJterm) - epoch()) of 65 | DeltaSecs when DeltaSecs > 0 -> 66 | false; 67 | _ -> 68 | true 69 | end 70 | end. 71 | 72 | parse_jwt_check_sig(<<"JWT">>, Alg, Header, ClaimSet, Signature, Key) -> 73 | Payload = <
>, 74 | jwt_sign(Alg, Payload, Key) =:= Signature. 75 | 76 | split_jwt_token(Token) -> 77 | binary:split(Token, [<<".">>], [global]). 78 | 79 | decode_jwt([Header, ClaimSet, Signature]) -> 80 | [HeaderJterm, ClaimSetJterm] = 81 | Decoded = [jiffy_decode_safe(base64url:decode(X)) || X <- [Header, ClaimSet]], 82 | case lists:any(fun(E) -> E =:= invalid end, Decoded) of 83 | true -> invalid; 84 | false -> {HeaderJterm, ClaimSetJterm, Signature} 85 | end; 86 | decode_jwt(_) -> 87 | invalid. 88 | 89 | parse_jwt_iss_sub(Token, Key) -> 90 | case parse_jwt(Token, Key) of 91 | invalid -> 92 | invalid; 93 | expired -> 94 | expired; 95 | ClaimSetJterm -> 96 | {ej:get({<<"iss">>}, ClaimSetJterm), ej:get({<<"sub">>}, ClaimSetJterm)} 97 | end. 98 | 99 | jwt(Alg, ClaimSetJterm, Key) -> 100 | ClaimSet = base64url:encode(jiffy:encode(ClaimSetJterm)), 101 | Header = base64url:encode(jiffy:encode(jwt_header(Alg))), 102 | Payload = <
>, 103 | case jwt_sign(Alg, Payload, Key) of 104 | alg_not_supported -> 105 | alg_not_supported; 106 | Signature -> 107 | <> 108 | end. 109 | 110 | 111 | jwt(Alg, ClaimSetJterm, ExpirationSeconds, Key) -> 112 | ClaimSet = base64url:encode(jiffy:encode(jwt_add_exp(ClaimSetJterm, ExpirationSeconds))), 113 | Header = base64url:encode(jiffy:encode(jwt_header(Alg))), 114 | Payload = <
>, 115 | case jwt_sign(Alg, Payload, Key) of 116 | alg_not_supported -> 117 | alg_not_supported; 118 | Signature -> 119 | <> 120 | end. 121 | 122 | jwt_add_exp(ClaimSetJterm, ExpirationSeconds) -> 123 | {ClaimsSet} = ClaimSetJterm, 124 | Expiration = case ExpirationSeconds of 125 | {hourly, ExpirationSeconds0} -> 126 | Ts = epoch(), 127 | (Ts - (Ts rem 3600)) + ExpirationSeconds0; 128 | {daily, ExpirationSeconds0} -> 129 | Ts = epoch(), 130 | (Ts - (Ts rem (24*3600))) + ExpirationSeconds0; 131 | _ -> 132 | epoch() + ExpirationSeconds 133 | end, 134 | {[{<<"exp">>, Expiration} | ClaimsSet]}. 135 | 136 | jwt_hs256_iss_sub(Iss, Sub, ExpirationSeconds, Key) -> 137 | jwt(<<"HS256">>, {[ 138 | {<<"iss">>, Iss}, 139 | {<<"sub">>, Sub} 140 | ]}, ExpirationSeconds, Key). 141 | 142 | jwt_sign(<<"HS256">>, Payload, Key) -> 143 | base64url:encode(crypto:hmac(sha256, Key, Payload)); 144 | 145 | jwt_sign(_, _, _) -> 146 | alg_not_supported. 147 | 148 | jwt_header(Alg) -> 149 | {[ 150 | {<<"alg">>, Alg}, 151 | {<<"typ">>, <<"JWT">>} 152 | ]}. 153 | 154 | epoch() -> 155 | calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(os:timestamp())) - 719528 * 24 * 3600. 156 | --------------------------------------------------------------------------------