Packages

A small, focused OAuth 2.0 library for Gleam, built on top of Gleams HTTP requests and responses.

Current section

Files

Jump to
flwr_oauth2 src flwr_oauth2.erl
Raw

src/flwr_oauth2.erl

-module(flwr_oauth2).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/flwr_oauth2.gleam").
-export([to_string_token_response/1, to_string_client_authentication/1, to_string_token_request/1, add_scope/2, setup_request/3, parse_error_response/1, secret_is_valid/1, is_secret_invalid/1, authorization_setter/1, to_http_request/1, parse_scope/1, parse_token_response/1]).
-export_type([client_id/0, secret/0, token_request/0, client_authentication/0, request_error/0, response_error/0, access_token_response/0, authorization_setter/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" This module implements functions and types for [RFC6749](https://datatracker.ietf.org/doc/html/rfc6749).\n"
" It offers types for all the major OAuth 2.0 grant types and functions to create the correct HTTP requests for those grant types.\n"
" Furthermore, it offers functions to generate redirect URIs for the [Authorization Code Grant Type](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) and the [Implicit Grant Type](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2).\n"
"\n"
" It also supports [RFC7636](https://datatracker.ietf.org/doc/html/rfc7636), which adds the Proof Key for Code Exchange to the Authorization Code Grant.\n"
).
-type client_id() :: {client_id, binary()}.
-type secret() :: {secret, binary()} |
{secret_with_expiration, binary(), gleam@time@timestamp:timestamp()}.
-type token_request() :: {authorization_code_grant_token_request,
gleam@uri:uri(),
client_authentication(),
gleam@option:option(gleam@uri:uri()),
binary()} |
{authorization_code_grant_token_request_with_p_k_c_e,
gleam@uri:uri(),
client_authentication(),
gleam@option:option(gleam@uri:uri()),
binary(),
binary()} |
{resource_owner_credentials_grant_token_request,
gleam@uri:uri(),
client_authentication(),
binary(),
binary(),
list(binary())} |
{refresh_token_grant_request,
gleam@uri:uri(),
client_authentication(),
binary(),
list(binary())} |
{client_credentials_grant_token_request,
gleam@uri:uri(),
client_authentication(),
list(binary())}.
-type client_authentication() :: {client_secret_basic, client_id(), secret()} |
{client_secret_post, client_id(), secret()} |
{public_authentication, client_id()}.
-type request_error() :: secret_expired | invalid_uri.
-type response_error() :: {error_response,
integer(),
binary(),
gleam@option:option(binary()),
gleam@option:option(binary())} |
{parse_error, gleam@json:decode_error()}.
-type access_token_response() :: {access_token_response,
binary(),
binary(),
gleam@option:option(integer()),
gleam@option:option(binary()),
list(binary())}.
-type authorization_setter() :: {authorization_setter,
fun((gleam@http@request:request(list({binary(), binary()}))) -> {ok,
gleam@http@request:request(list({binary(), binary()}))} |
{error, request_error()})}.
-file("src/flwr_oauth2.gleam", 168).
?DOC(" Creates a String representation of a token request\n").
-spec to_string_token_response(access_token_response()) -> binary().
to_string_token_response(Resp) ->
<<<<<<<<<<<<<<<<<<<<"AccessTokenResponse(
access_token: "/utf8,
(erlang:element(2, Resp))/binary>>/binary,
",
token_type: "/utf8>>/binary,
(erlang:element(3, Resp))/binary>>/binary,
",
expires_in: "/utf8>>/binary,
(gleam@option:unwrap(
gleam@option:map(
erlang:element(4, Resp),
fun erlang:integer_to_binary/1
),
<<"None"/utf8>>
))/binary>>/binary,
"),
refresh_token: "/utf8>>/binary,
(gleam@option:unwrap(
erlang:element(5, Resp),
<<"None"/utf8>>
))/binary>>/binary,
",
scope: ["/utf8>>/binary,
(gleam@string:join(erlang:element(6, Resp), <<", "/utf8>>))/binary>>/binary,
"],
)"/utf8>>.
-file("src/flwr_oauth2.gleam", 287).
-spec to_string_client_authentication(client_authentication()) -> binary().
to_string_client_authentication(Client_auth) ->
case Client_auth of
{client_secret_basic, Client_id, _} ->
<<<<<<<<<<"ClientSecretBasic("/utf8, "client_id="/utf8>>/binary,
(erlang:element(2, Client_id))/binary>>/binary,
", "/utf8>>/binary,
"client_secret=***"/utf8>>/binary,
")"/utf8>>;
{client_secret_post, Client_id@1, _} ->
<<<<<<<<<<"ClientSecretPost("/utf8, "client_id="/utf8>>/binary,
(erlang:element(2, Client_id@1))/binary>>/binary,
", "/utf8>>/binary,
"client_secret=***"/utf8>>/binary,
")"/utf8>>;
{public_authentication, Client_id@2} ->
<<<<<<"PublicAuthentication("/utf8, "client_id="/utf8>>/binary,
(erlang:element(2, Client_id@2))/binary>>/binary,
")"/utf8>>
end.
-file("src/flwr_oauth2.gleam", 181).
-spec to_string_token_request(token_request()) -> binary().
to_string_token_request(Req) ->
case Req of
{authorization_code_grant_token_request,
Token_endpoint,
Authentication,
Redirect_uri,
Code} ->
<<<<<<<<<<<<<<<<<<<<<<<<"AuthorizationCodeGrantTokenRequest("/utf8,
"token_endpoint="/utf8>>/binary,
(gleam@uri:to_string(
Token_endpoint
))/binary>>/binary,
", "/utf8>>/binary,
"authentication="/utf8>>/binary,
(to_string_client_authentication(
Authentication
))/binary>>/binary,
", "/utf8>>/binary,
"redirect_uri="/utf8>>/binary,
(begin
_pipe = gleam@option:map(
Redirect_uri,
fun gleam@uri:to_string/1
),
gleam@option:unwrap(_pipe, <<"None"/utf8>>)
end)/binary>>/binary,
", "/utf8>>/binary,
"code="/utf8>>/binary,
Code/binary>>/binary,
")"/utf8>>;
{authorization_code_grant_token_request_with_p_k_c_e,
Token_endpoint@1,
Authentication@1,
Redirect_uri@1,
Code@1,
Code_verifier} ->
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"AuthorizationCodeGrantTokenRequestWithPKCE("/utf8,
"token_endpoint="/utf8>>/binary,
(gleam@uri:to_string(
Token_endpoint@1
))/binary>>/binary,
", "/utf8>>/binary,
"authentication="/utf8>>/binary,
(to_string_client_authentication(
Authentication@1
))/binary>>/binary,
", "/utf8>>/binary,
"redirect_uri="/utf8>>/binary,
(begin
_pipe@1 = gleam@option:map(
Redirect_uri@1,
fun gleam@uri:to_string/1
),
gleam@option:unwrap(
_pipe@1,
<<"None"/utf8>>
)
end)/binary>>/binary,
", "/utf8>>/binary,
"code="/utf8>>/binary,
Code@1/binary>>/binary,
", "/utf8>>/binary,
"code_verifier="/utf8>>/binary,
Code_verifier/binary>>/binary,
")"/utf8>>;
{resource_owner_credentials_grant_token_request,
Token_endpoint@2,
Authentication@2,
Username,
Password,
Scope} ->
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"ResourceOwnerCredentialsGrantTokenRequest("/utf8,
"token_endpoint="/utf8>>/binary,
(gleam@uri:to_string(
Token_endpoint@2
))/binary>>/binary,
", "/utf8>>/binary,
"authentication="/utf8>>/binary,
(to_string_client_authentication(
Authentication@2
))/binary>>/binary,
", "/utf8>>/binary,
"username="/utf8>>/binary,
Username/binary>>/binary,
", "/utf8>>/binary,
"password="/utf8>>/binary,
Password/binary>>/binary,
", "/utf8>>/binary,
"scope="/utf8>>/binary,
(gleam@string:join(Scope, <<" "/utf8>>))/binary>>/binary,
")"/utf8>>;
{refresh_token_grant_request,
Token_endpoint@3,
Authentication@3,
Refresh_token,
Scope@1} ->
<<<<<<<<<<<<<<<<<<<<<<<<"RefreshTokenGrantRequest("/utf8,
"token_endpoint="/utf8>>/binary,
(gleam@uri:to_string(
Token_endpoint@3
))/binary>>/binary,
", "/utf8>>/binary,
"authentication="/utf8>>/binary,
(to_string_client_authentication(
Authentication@3
))/binary>>/binary,
", "/utf8>>/binary,
"refresh_token="/utf8>>/binary,
Refresh_token/binary>>/binary,
", "/utf8>>/binary,
"scope="/utf8>>/binary,
(gleam@string:join(Scope@1, <<" "/utf8>>))/binary>>/binary,
")"/utf8>>;
{client_credentials_grant_token_request,
Token_endpoint@4,
Authentication@4,
Scope@2} ->
<<<<<<<<<<<<<<<<<<"ClientCredentialsGrantTokenRequest("/utf8,
"token_endpoint="/utf8>>/binary,
(gleam@uri:to_string(
Token_endpoint@4
))/binary>>/binary,
", "/utf8>>/binary,
"authentication="/utf8>>/binary,
(to_string_client_authentication(
Authentication@4
))/binary>>/binary,
", "/utf8>>/binary,
"scope="/utf8>>/binary,
(gleam@string:join(Scope@2, <<" "/utf8>>))/binary>>/binary,
")"/utf8>>
end.
-file("src/flwr_oauth2.gleam", 376).
?DOC(" Function that adds a scope to a list if the scope is not empty.\n").
-spec add_scope(list({binary(), binary()}), list(binary())) -> list({binary(),
binary()}).
add_scope(D, Scope) ->
_pipe@1 = case gleam@list:is_empty(Scope) of
false ->
{some,
{<<"scope"/utf8>>,
begin
_pipe = Scope,
gleam@string:join(_pipe, <<" "/utf8>>)
end}};
true ->
none
end,
flwr_oauth2@helpers:add_if_present(D, _pipe@1).
-file("src/flwr_oauth2.gleam", 389).
?DOC(
" A helper function that maps a general request with credentials to a gleam http request.\n"
" The credentials are either attached to the request body URL encoded or added as basic `authorization` header.\n"
).
-spec setup_request(
gleam@uri:uri(),
list({binary(), binary()}),
authorization_setter()
) -> {ok, gleam@http@request:request(binary())} | {error, request_error()}.
setup_request(Endpoint, Body, Client_auth) ->
Req = begin
_pipe = gleam@http@request:from_uri(Endpoint),
gleam@result:map_error(_pipe, fun(_) -> invalid_uri end)
end,
_pipe@7 = begin
gleam@result:map(
Req,
fun(Req@1) ->
Req@2 = begin
_pipe@1 = Req@1,
_pipe@2 = gleam@http@request:set_method(_pipe@1, post),
_pipe@3 = gleam@http@request:set_header(
_pipe@2,
<<"content-type"/utf8>>,
<<"application/x-www-form-urlencoded"/utf8>>
),
_pipe@4 = gleam@http@request:set_body(_pipe@3, Body),
(erlang:element(2, Client_auth))(_pipe@4)
end,
gleam@result:map(
Req@2,
fun(Req@3) -> _pipe@5 = erlang:element(4, Req@3),
_pipe@6 = gleam@uri:query_to_string(_pipe@5),
gleam@http@request:set_body(Req@3, _pipe@6) end
)
end
)
end,
gleam@result:flatten(_pipe@7).
-file("src/flwr_oauth2.gleam", 506).
?DOC(" Parses an OAuth 2.0 error response defined in [RFC6749 Error Response](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2).\n").
-spec parse_error_response(gleam@http@response:response(binary())) -> response_error().
parse_error_response(Response) ->
Error_decoder = begin
gleam@dynamic@decode:field(
<<"error"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Error) ->
gleam@dynamic@decode:optional_field(
<<"error_description"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
fun(Error_description) ->
gleam@dynamic@decode:optional_field(
<<"error_uri"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Error_uri) ->
gleam@dynamic@decode:success(
{error_response,
erlang:element(2, Response),
Error,
Error_description,
Error_uri}
)
end
)
end
)
end
)
end,
_pipe = gleam@json:parse(erlang:element(4, Response), Error_decoder),
_pipe@1 = gleam@result:map_error(
_pipe,
fun(Field@0) -> {parse_error, Field@0} end
),
flwr_oauth2@helpers:unwrap_both(_pipe@1).
-file("src/flwr_oauth2.gleam", 535).
?DOC(
" Checks if a given secret is not expired.\n"
" Returns always true for secrets that cannot expire.\n"
).
-spec secret_is_valid(secret()) -> boolean().
secret_is_valid(Secret) ->
_pipe = case Secret of
{secret, _} ->
true;
{secret_with_expiration, _, Expires_at} ->
gleam@time@timestamp:compare(
Expires_at,
gleam@time@timestamp:system_time()
)
=:= lt
end,
gleam@bool:negate(_pipe).
-file("src/flwr_oauth2.gleam", 546).
?DOC(
" Checks if a given secret is expired.\n"
" Returns always false for secrets that cannot expire.\n"
).
-spec is_secret_invalid(secret()) -> boolean().
is_secret_invalid(Secret) ->
case Secret of
{secret, _} ->
false;
{secret_with_expiration, _, _} ->
_pipe = Secret,
_pipe@1 = secret_is_valid(_pipe),
gleam@bool:negate(_pipe@1)
end.
-file("src/flwr_oauth2.gleam", 415).
?DOC(
" Encodes the ClientAuthentication that is to be sent to the OAuth 2.0 Server.\n"
" For Basic Authentication it will always encode it with base64.\n"
).
-spec authorization_setter(client_authentication()) -> authorization_setter().
authorization_setter(Auth) ->
_pipe@10 = case Auth of
{client_secret_basic, Client_id, Client_secret} ->
fun(Req) ->
gleam@bool:guard(
begin
_pipe = Client_secret,
is_secret_invalid(_pipe)
end,
{error, secret_expired},
fun() -> _pipe@1 = Req,
_pipe@2 = flwr_oauth2@http_headers:set_basic(
_pipe@1,
erlang:element(2, Client_id),
erlang:element(2, Client_secret)
),
{ok, _pipe@2} end
)
end;
{client_secret_post, Client_id@1, Client_secret@1} ->
fun(Req@1) ->
gleam@bool:guard(
begin
_pipe@3 = Client_secret@1,
is_secret_invalid(_pipe@3)
end,
{error, secret_expired},
fun() ->
Body = begin
_pipe@4 = erlang:element(4, Req@1),
lists:append(
[{<<"client_id"/utf8>>,
erlang:element(2, Client_id@1)},
{<<"client_secret"/utf8>>,
erlang:element(2, Client_secret@1)}],
_pipe@4
)
end,
_pipe@5 = Req@1,
_pipe@6 = gleam@http@request:set_body(_pipe@5, Body),
{ok, _pipe@6}
end
)
end;
{public_authentication, Client_id@2} ->
fun(Req@2) ->
Body@1 = begin
_pipe@7 = erlang:element(4, Req@2),
lists:append(
[{<<"client_id"/utf8>>, erlang:element(2, Client_id@2)}],
_pipe@7
)
end,
_pipe@8 = Req@2,
_pipe@9 = gleam@http@request:set_body(_pipe@8, Body@1),
{ok, _pipe@9}
end
end,
{authorization_setter, _pipe@10}.
-file("src/flwr_oauth2.gleam", 314).
?DOC(
" Creates a http request from the given TokenRequest, but does not send.\n"
" Sending the request is done by the user of the function.\n"
).
-spec to_http_request(token_request()) -> {ok,
gleam@http@request:request(binary())} |
{error, request_error()}.
to_http_request(Request) ->
_pipe@5 = case Request of
{authorization_code_grant_token_request, _, _, Redirect_uri, Code} ->
Redirect_uri@1 = flwr_oauth2@helpers:encode_redirect_uri(
Redirect_uri
),
_pipe = [{<<"grant_type"/utf8>>, <<"authorization_code"/utf8>>},
{<<"code"/utf8>>, Code}],
flwr_oauth2@helpers:add_if_present(_pipe, Redirect_uri@1);
{authorization_code_grant_token_request_with_p_k_c_e,
_,
_,
Redirect_uri@2,
Code@1,
Code_verifier} ->
Redirect_uri@3 = flwr_oauth2@helpers:encode_redirect_uri(
Redirect_uri@2
),
_pipe@1 = [{<<"grant_type"/utf8>>, <<"authorization_code"/utf8>>},
{<<"code"/utf8>>, Code@1},
{<<"code_verifier"/utf8>>, Code_verifier}],
flwr_oauth2@helpers:add_if_present(_pipe@1, Redirect_uri@3);
{resource_owner_credentials_grant_token_request,
_,
_,
Username,
Password,
Scope} ->
_pipe@2 = [{<<"grant_type"/utf8>>, <<"password"/utf8>>},
{<<"username"/utf8>>, Username},
{<<"password"/utf8>>, Password}],
add_scope(_pipe@2, Scope);
{refresh_token_grant_request, _, _, Refresh_token, Scope@1} ->
_pipe@3 = [{<<"grant_type"/utf8>>, <<"refresh_token"/utf8>>},
{<<"refresh_token"/utf8>>, Refresh_token}],
add_scope(_pipe@3, Scope@1);
{client_credentials_grant_token_request, _, _, Scope@2} ->
_pipe@4 = [{<<"grant_type"/utf8>>, <<"client_credentials"/utf8>>}],
add_scope(_pipe@4, Scope@2)
end,
setup_request(
erlang:element(2, Request),
_pipe@5,
authorization_setter(erlang:element(3, Request))
).
-file("src/flwr_oauth2.gleam", 566).
-spec string_not_empty(binary()) -> boolean().
string_not_empty(L) ->
not gleam@string:is_empty(L).
-file("src/flwr_oauth2.gleam", 559).
?DOC(
" Parses a string containing the space separated scopes.\n"
"\n"
" ## Example\n"
" ```gleam\n"
" parse_scope(\"scope1 scope2\")\n"
" ````\n"
).
-spec parse_scope(binary()) -> list(binary()).
parse_scope(Scope) ->
_pipe = Scope,
_pipe@1 = gleam@string:trim(_pipe),
_pipe@2 = gleam@string:split(_pipe@1, <<" "/utf8>>),
gleam@list:filter(_pipe@2, fun string_not_empty/1).
-file("src/flwr_oauth2.gleam", 471).
-spec parse_token_success_response(gleam@http@response:response(binary())) -> {ok,
access_token_response()} |
{error, response_error()}.
parse_token_success_response(Response) ->
Token_decoder = begin
gleam@dynamic@decode:field(
<<"access_token"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Access_token) ->
gleam@dynamic@decode:field(
<<"token_type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Token_type) ->
gleam@dynamic@decode:optional_field(
<<"expires_in"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder, fun gleam@dynamic@decode:decode_int/1}
),
fun(Expires_in) ->
gleam@dynamic@decode:optional_field(
<<"refresh_token"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Refresh_token) ->
gleam@dynamic@decode:optional_field(
<<"scope"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Scope) ->
Scope@1 = begin
_pipe = Scope,
_pipe@1 = gleam@option:map(
_pipe,
fun parse_scope/1
),
gleam@option:unwrap(
_pipe@1,
[]
)
end,
gleam@dynamic@decode:success(
{access_token_response,
Access_token,
Token_type,
Expires_in,
Refresh_token,
Scope@1}
)
end
)
end
)
end
)
end
)
end
)
end,
_pipe@2 = gleam@json:parse(erlang:element(4, Response), Token_decoder),
gleam@result:map_error(_pipe@2, fun(Field@0) -> {parse_error, Field@0} end).
-file("src/flwr_oauth2.gleam", 462).
?DOC(" Parses a token response and returns the access and refresh token if valid response, otherwise the error response.\n").
-spec parse_token_response(gleam@http@response:response(binary())) -> {ok,
access_token_response()} |
{error, response_error()}.
parse_token_response(Response) ->
case erlang:element(2, Response) of
200 ->
parse_token_success_response(Response);
_ ->
_pipe = parse_error_response(Response),
{error, _pipe}
end.