-module(flwr_oauth2). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/flwr_oauth2.gleam"). -export([add_scope/2, setup_request/3, parse_error_response/1, secret_is_valid/1, is_secret_invalid/1, authorization_setter/1, parse_scope/1, parse_token_response/1, make_redirect_uri/1, to_http_request/1, random_state/1, random_state32/0]). -export_type([response_type/0, client_id/0, secret/0, state/0, authorization_code_grant_redirect_uri/0, code_challenge_method/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 response_type() :: code | token. -type client_id() :: {client_id, binary()}. -type secret() :: {secret, binary()} | {secret_with_expiration, binary(), gleam@time@timestamp:timestamp()}. -type state() :: {state, binary()}. -type authorization_code_grant_redirect_uri() :: {authorization_code_grant_redirect_uri, gleam@uri:uri(), response_type(), gleam@option:option(gleam@uri:uri()), client_id(), list(binary()), gleam@option:option(state())} | {authorization_code_grant_redirect_uri_with_p_k_c_e, gleam@uri:uri(), response_type(), gleam@option:option(gleam@uri:uri()), client_id(), list(binary()), gleam@option:option(state()), binary(), code_challenge_method()}. -type code_challenge_method() :: plain | s256. -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", 321). ?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", 334). ?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", 451). ?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", 480). ?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", 491). ?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", 360). ?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", 525). -spec response_type_to_string(response_type()) -> binary(). response_type_to_string(Response_type) -> case Response_type of code -> <<"code"/utf8>>; token -> <<"token"/utf8>> end. -file("src/flwr_oauth2.gleam", 532). -spec string_not_empty(binary()) -> boolean(). string_not_empty(L) -> not gleam@string:is_empty(L). -file("src/flwr_oauth2.gleam", 504). ?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", 416). -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", 407). ?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. -file("src/flwr_oauth2.gleam", 536). -spec add_many_if_present(list(FPU), gleam@option:option(list(FPU))) -> list(FPU). add_many_if_present(D, Value) -> _pipe = Value, _pipe@1 = gleam@option:unwrap(_pipe, []), lists:append(_pipe@1, D). -file("src/flwr_oauth2.gleam", 550). -spec wrap_tuple(FQB, FQC) -> {FQB, FQC}. wrap_tuple(Name, Value) -> {Name, Value}. -file("src/flwr_oauth2.gleam", 542). -spec to_redirect_uri_query(gleam@option:option(gleam@uri:uri())) -> gleam@option:option({binary(), binary()}). to_redirect_uri_query(Value) -> _pipe = Value, _pipe@1 = gleam@option:map(_pipe, fun gleam@uri:to_string/1), gleam@option:map( _pipe@1, fun(_capture) -> wrap_tuple(<<"redirect_uri"/utf8>>, _capture) end ). -file("src/flwr_oauth2.gleam", 214). ?DOC(" Creates the uri that the resource owner should be redirected too.\n"). -spec make_redirect_uri(authorization_code_grant_redirect_uri()) -> gleam@uri:uri(). make_redirect_uri(Redirect_config) -> State = begin _pipe = erlang:element(7, Redirect_config), _pipe@1 = gleam@option:map(_pipe, fun(X) -> erlang:element(2, X) end), gleam@option:map( _pipe@1, fun(_capture) -> wrap_tuple(<<"state"/utf8>>, _capture) end ) end, Redirect_uri = to_redirect_uri_query(erlang:element(4, Redirect_config)), Code_callenge = case Redirect_config of {authorization_code_grant_redirect_uri_with_p_k_c_e, _, _, _, _, _, _, Code_challenge, Method} -> {some, [{<<"code_challenge"/utf8>>, Code_challenge}, {<<"code_challenge_method"/utf8>>, case Method of plain -> <<"plain"/utf8>>; s256 -> <<"S256"/utf8>> end}]}; _ -> none end, Queries = begin _pipe@2 = [{<<"response_type"/utf8>>, response_type_to_string(erlang:element(3, Redirect_config))}, {<<"client_id"/utf8>>, erlang:element(2, erlang:element(5, Redirect_config))}, {<<"scope"/utf8>>, gleam@string:join( erlang:element(6, Redirect_config), <<" "/utf8>> )}], _pipe@3 = flwr_oauth2@helpers:add_if_present(_pipe@2, Redirect_uri), _pipe@4 = flwr_oauth2@helpers:add_if_present(_pipe@3, State), add_many_if_present(_pipe@4, Code_callenge) end, _record = erlang:element(2, Redirect_config), {uri, erlang:element(2, _record), erlang:element(3, _record), erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), {some, gleam@uri:query_to_string(Queries)}, erlang:element(8, _record)}. -file("src/flwr_oauth2.gleam", 259). ?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 = to_redirect_uri_query(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 = to_redirect_uri_query(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", 515). ?DOC( " Generates a random State with the specified length including only uppercase and lowercase letters\n" " If length <= 0 returns an empty string\n" ). -spec random_state(integer()) -> state(). random_state(Length) -> _pipe = flwr_oauth2@helpers:generate_random_string( <<"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"/utf8>>, Length ), {state, _pipe}. -file("src/flwr_oauth2.gleam", 521). ?DOC(" Generates a random 32 character long State\n"). -spec random_state32() -> state(). random_state32() -> random_state(32).