Packages
oidcc
3.0.0-alpha.1
3.7.2
3.7.1
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
retired
3.4.0
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.2.0-beta.3
3.2.0-beta.2
retired
3.2.0-beta.1
retired
3.1.2
3.1.2-beta.1
retired
3.1.1
retired
3.1.0
retired
3.1.0-beta.2
retired
3.1.0-beta.1
retired
3.0.2
3.0.1
retired
3.0.0
retired
3.0.0-rc.6
retired
3.0.0-rc.5
retired
3.0.0-rc.4
retired
3.0.0-rc.3
retired
3.0.0-rc.2
retired
3.0.0-rc.1
retired
3.0.0-alpha.5
retired
3.0.0-alpha.4
retired
3.0.0-alpha.3
retired
3.0.0-alpha.2
retired
3.0.0-alpha.1
retired
2.0.0-alpha.2
retired
2.0.0-alpha.1
retired
1.8.1
retired
1.8.0
retired
1.7.0
retired
1.6.0
retired
1.5.1
retired
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.1
retired
1.2.0
retired
1.1.0
retired
1.0.1
retired
1.0.0
retired
OpenID Connect client library for the BEAM.
Retired package: Security issue - DoS Vulnerability, see https://github.com/erlef/oidcc/security/advisories/GHSA-mj35-2rgf-cv8p
Current section
Files
Jump to
Current section
Files
src/oidcc_http_util.erl
%%%-------------------------------------------------------------------
%% @doc HTTP Client Utilities
%% @end
%%%-------------------------------------------------------------------
-module(oidcc_http_util).
-feature(maybe_expr, enable).
-export([basic_auth_header/2]).
-export([bearer_auth_header/1]).
-export([request/4]).
-export_type([
http_header/0, error/0, httpc_error/0, query_params/0, telemetry_opts/0, request_opts/0
]).
-type query_params() :: [{unicode:chardata(), unicode:chardata() | true}].
%% See {@link uri_string:compose_query/1}
-type http_header() :: {Field :: [byte()], Value :: iodata()}.
%% See {@link httpc:request/5}
-type error() ::
{http_error, StatusCode :: pos_integer(), HttpBodyResult :: binary()}
| invalid_content_type
| httpc_error().
-type httpc_error() :: term().
%% See {@link httpc:request/5} for additional errors
-type request_opts() :: #{
timeout => timeout(),
ssl => [ssl:tls_option()]
}.
%% See {@link httpc:request/5}
%%
%% <h2>Parameters</h2>
%%
%% <ul>
%% <li>`timeout' - timeout for request</li>
%% <li>`ssl' - TLS config</li>
%% </ul>
-type telemetry_opts() :: #{
topic := [atom()],
extra_meta => map()
}.
%% @private
-spec basic_auth_header(User, Secret) -> http_header() when
User :: binary(),
Secret :: binary().
basic_auth_header(User, Secret) ->
UserEnc = uri_string:compose_query([{User, true}]),
SecretEnc = uri_string:compose_query([{Secret, true}]),
RawAuth = <<UserEnc/binary, <<":">>/binary, SecretEnc/binary>>,
AuthData = base64:encode(RawAuth),
{"authorization", [<<"Basic ">>, AuthData]}.
%% @private
-spec bearer_auth_header(Token) -> http_header() when Token :: binary().
bearer_auth_header(Token) ->
{"authorization", [<<"Bearer ">>, Token]}.
%% @private
-spec request(Method, Request, TelemetryOpts, RequestOpts) ->
{ok, {{json, term()} | {jwt, binary()}, [http_header()]}}
| {error, error()}
when
Method :: head | get | put | patch | post | trace | options | delete,
Request ::
{uri_string:uri_string(), [http_header()]}
| {
uri_string:uri_string(),
[http_header()],
ContentType :: uri_string:uri_string(),
HttpBody
},
HttpBody ::
iolist()
| binary()
| {
fun((Accumulator :: term()) -> eof | {ok, iolist(), Accumulator :: term()}),
Accumulator :: term()
}
| {chunkify, fun((Accumulator :: term()) -> eof | {ok, iolist(), Accumulator :: term()}),
Accumulator :: term()},
TelemetryOpts :: telemetry_opts(),
RequestOpts :: request_opts().
request(Method, Request, TelemetryOpts, RequestOpts) ->
TelemetryTopic = maps:get(topic, TelemetryOpts),
TelemetryExtraMeta = maps:get(extra_meta, TelemetryOpts, #{}),
Timeout = maps:get(timeout, RequestOpts, timer:minutes(1)),
SslOpts = maps:get(ssl, RequestOpts, undefined),
HttpOpts0 = [{timeout, Timeout}],
HttpOpts = case SslOpts of
undefined -> HttpOpts0;
_Opts -> [{ssl, SslOpts} | HttpOpts0]
end,
telemetry:span(
TelemetryTopic,
TelemetryExtraMeta,
fun() ->
maybe
{ok, {_StatusLine, Headers, _Result} = Response} ?=
httpc:request(Method,
Request,
HttpOpts,
[{body_format, binary}]),
{ok, BodyAndFormat} ?= extract_successful_response(Response),
{{ok, {BodyAndFormat, Headers}}, TelemetryExtraMeta}
else
{error, Reason} ->
{{error, Reason}, maps:put(error, Reason, TelemetryExtraMeta)}
end
end
).
-spec extract_successful_response({StatusLine, [HttpHeader], HttpBodyResult}) ->
{ok, {json, term()} | {jwt, binary()}} | {error, error()}
when
StatusLine :: {HttpVersion, StatusCode, string()},
HttpVersion :: uri_string:uri_string(),
StatusCode :: pos_integer(),
HttpHeader :: http_header(),
HttpBodyResult :: binary().
extract_successful_response({{_HttpVersion, 200, _HttpStatusName}, Headers, HttpBodyResult}) ->
case fetch_content_type(Headers) of
json ->
{ok, {json, jose:decode(HttpBodyResult)}};
jwt ->
{ok, {jwt, HttpBodyResult}};
unknown ->
{error, invalid_content_type}
end;
extract_successful_response({{_HttpVersion, StatusCode, _HttpStatusName}, Headers, HttpBodyResult}) ->
Body =
case fetch_content_type(Headers) of
json ->
jose:decode(HttpBodyResult);
jwt ->
HttpBodyResult;
unknown ->
HttpBodyResult
end,
{error, {http_error, StatusCode, Body}}.
-spec fetch_content_type(Headers) -> json | jwt | unknown when Headers :: [http_header()].
fetch_content_type(Headers) ->
case proplists:lookup("content-type", Headers) of
{"content-type", "application/json" ++ _Rest} ->
json;
{"content-type", "application/jwt" ++ _Rest} ->
jwt;
_Other ->
unknown
end.