Packages
oidcc
1.6.0
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: Deprecated - Unmaintained major version, update
Current section
Files
Jump to
Current section
Files
src/oidcc_openid_provider.erl
-module(oidcc_openid_provider).
-behaviour(gen_server).
%% API.
-export([start_link/2]).
-export([stop/1]).
-export([is_issuer/2]).
-export([is_ready/1]).
-export([get_config/1]).
-export([update_config/1]).
-export([update_and_get_keys/1]).
-export([get_error/1]).
%% gen_server.
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).
-record(state, {
ready = false,
error = undefined,
key_requests = [],
registration_params = #{},
id = undefined,
name = undefined,
desc = undefined,
client_id = undefined,
client_secret = undefined,
request_scopes = undefined,
issuer = undefined,
config_ep = undefined,
config = #{},
keys = [],
lasttime_updated = undefined,
local_endpoint = undefined,
meta_data = #{},
config_tries = 1,
config_deadline = undefined,
http_result = undefined,
retrieving = undefined,
request_id = undefined,
extra_config = #{}
}).
%% API.
-spec start_link(Id :: binary(), Config::map()) -> {ok, pid()}.
start_link(Id, Config) ->
gen_server:start_link(?MODULE, {Id, Config}, []).
-spec stop(Pid ::pid()) -> ok.
stop(Pid) ->
gen_server:cast(Pid, stop).
-spec update_config(Pid :: pid() ) -> ok.
update_config(Pid) ->
gen_server:call(Pid, update_config).
-spec is_issuer(Issuer :: binary(), Pid :: pid() ) -> true | false.
is_issuer(Issuer, Pid) ->
gen_server:call(Pid, {is_issuer, Issuer}).
-spec is_ready(Pid :: pid() ) -> true | false.
is_ready(Pid) ->
gen_server:call(Pid, is_ready).
-spec get_config( Pid :: pid() ) -> {ok, Config :: map()}.
get_config( Pid) ->
gen_server:call(Pid, get_config).
-spec update_and_get_keys( Pid :: pid() ) -> {ok, Keys :: map()}.
update_and_get_keys(Pid) ->
gen_server:call(Pid, update_and_get_keys, 60000).
-spec get_error( Pid :: pid() ) -> {ok, term()}.
get_error( Pid) ->
gen_server:call(Pid, get_error).
%% timeout in seconds
-define(TIMEOUT, 60).
-define(GEN_TIMEOUT, ?TIMEOUT * 1000).
%% gen_server.
init({Id, Config}) ->
#{name := Name,
description := Description,
request_scopes := Scopes,
issuer_or_endpoint := IssuerOrEndpoint,
local_endpoint := LocalEndpoint
} = Config,
RegistrationParams = maps:get(registration_params, Config, #{}),
ClientSecret = maps:get(client_secret, Config, undefined),
ClientId = case ClientSecret of
undefined ->
undefined;
_ ->
maps:get(client_id, Config, undefined)
end,
trigger_config_retrieval(),
DeleteKeys = [name, description, request_scopes, issuer_or_endpoint,
local_endpoint, client_secret, client_id],
ExtraConfig = maps:without(DeleteKeys, Config),
ConfigEndpoint = to_config_endpoint(IssuerOrEndpoint),
Issuer = config_ep_to_issuer(ConfigEndpoint),
{ok, #state{id = Id, name = Name, desc = Description, client_id = ClientId,
client_secret = ClientSecret, config_ep = ConfigEndpoint,
request_scopes = Scopes, local_endpoint = LocalEndpoint,
issuer = Issuer, registration_params = RegistrationParams,
extra_config = ExtraConfig
}}.
handle_call(get_config, _From, State) ->
trigger_config_retrieval_if_needed(State),
Conf = create_config(State),
{reply, {ok, Conf}, State, ?GEN_TIMEOUT};
handle_call(update_and_get_keys, From, #state{key_requests = Requests}=State) ->
trigger_key_retrieval(),
NewRequests = [ From | Requests ],
NewState = State#state{key_requests = NewRequests},
{noreply, NewState, ?GEN_TIMEOUT};
handle_call(get_error, _From, #state{error=Error} = State) ->
trigger_config_retrieval_if_needed(State),
{reply, {ok, Error}, State, ?GEN_TIMEOUT};
handle_call(update_config, _From, State) ->
ok = trigger_config_retrieval(),
{reply, ok, State#state{config_tries=0}, ?GEN_TIMEOUT};
handle_call({is_issuer, Issuer}, _From, #state{config=Config}=State) ->
trigger_config_retrieval_if_needed(State),
Result = (Issuer == maps:get(issuer, Config, undefined)),
{reply, Result , State, ?GEN_TIMEOUT};
handle_call(is_ready, _From, #state{ready=Ready}=State) ->
trigger_config_retrieval_if_needed(State),
{reply, Ready, State, ?GEN_TIMEOUT};
handle_call(_Request, _From, State) ->
trigger_config_retrieval_if_needed(State),
{reply, ignored, State, ?GEN_TIMEOUT}.
handle_cast(retrieve_config, #state{ request_id = undefined,
config_ep=ConfigEndpoint} = State) ->
NewState = http_async_get(config, ConfigEndpoint, [], State),
{noreply, NewState, ?GEN_TIMEOUT};
handle_cast(retrieve_config, State) ->
trigger_config_retrieval_if_needed(State),
{noreply, State#state{config_deadline=deadline_in(120)}, ?GEN_TIMEOUT};
handle_cast(retrieve_keys, #state{ request_id = undefined,
config = Config} = State) ->
trigger_config_retrieval_if_needed(State),
NewState =
case maps:get(jwks_uri, Config, undefined) of
undefined ->
State#state{error=no_jwk_uri};
KeyEndpoint ->
Header = [{"accept",
"application/json;q=0.7,application/jwk+json"}],
http_async_get(keys, KeyEndpoint, Header, State)
end,
{noreply, NewState, ?GEN_TIMEOUT};
handle_cast(retrieve_keys, State) ->
trigger_config_retrieval_if_needed(State),
trigger_key_retrieval(),
{noreply, State, ?GEN_TIMEOUT};
handle_cast(register_if_needed, #state{ request_id = undefined,
client_id = undefined,
local_endpoint=LocalEndpoint,
registration_params=RegistrationParams,
config = Config
} = State) ->
trigger_config_retrieval_if_needed(State),
BasicParams = #{application_type => <<"web">>,
redirect_uris => [LocalEndpoint]},
RegParams = maps:merge(RegistrationParams, BasicParams),
Body = jsone:encode(RegParams),
RegistrationEndpoint = maps:get(registration_endpoint, Config),
NewState = http_async_post(registration, RegistrationEndpoint, [],
"application/json", Body, State),
{noreply, NewState, ?GEN_TIMEOUT};
handle_cast(register_if_needed, State) ->
trigger_config_retrieval_if_needed(State),
{noreply, State#state{ready=true}, ?GEN_TIMEOUT};
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(_Msg, State) ->
trigger_config_retrieval_if_needed(State),
{noreply, State, ?GEN_TIMEOUT}.
handle_info({http, {RequestId, Result}}, #state{request_id = RequestId} =
State) ->
trigger_config_retrieval_if_needed(State),
NewState = handle_http_result(State#state{http_result = Result}),
{noreply, NewState, ?GEN_TIMEOUT};
handle_info(timeout, State) ->
trigger_config_retrieval_if_needed(State),
{noreply, State, ?GEN_TIMEOUT}.
http_async_get(Type, Url, Header, State) ->
case oidcc_http_util:async_http(get, Url, Header) of
{ok, RequestId} ->
State#state{request_id = RequestId, retrieving=Type};
Error ->
State#state{error = Error}
end.
http_async_post(Type, Url, Header, ContentType, Body, State) ->
case oidcc_http_util:async_http(post, Url, Header, ContentType, Body) of
{ok, RequestId} ->
State#state{request_id = RequestId, retrieving=Type};
Error ->
State#state{error = Error}
end.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
handle_http_result(true, _, Header, Body, config, State) ->
handle_config(Body, Header, State);
handle_http_result(true, _, Header, Body, keys, State) ->
handle_keys(Body, Header, State);
handle_http_result(true, _, Header, Body, registration, State) ->
handle_registration(Body, Header, State);
handle_http_result(false, Status, _Header, Body, Retrieve, State) ->
State#state{error = {retrieving, Retrieve, Status, Body},
config_deadline = deadline_in(600)}.
handle_http_result(#state{http_result={error, Reason}} = State) ->
handle_http_client_crash(Reason, State);
handle_http_result(#state{retrieving=Retrieve,
http_result={{_Proto, Status, _StatusName}, Header,
InBody}
} = State) ->
GoodStatus = (Status >= 200) and (Status < 300),
{ok, Body} = oidcc_http_util:uncompress_body_if_needed(InBody, Header),
handle_http_result(GoodStatus, Status, Header, Body, Retrieve, State).
create_config(#state{id = Id, desc = Desc, client_id = ClientId,
client_secret = ClientSecret, config_ep = ConfEp,
config=Config, keys = Keys, issuer = Issuer,
lasttime_updated = LastTimeUpdated, ready = Ready,
local_endpoint = LocalEndpoint, name = Name,
request_scopes = Scopes, meta_data = MetaData,
config_deadline = ConfDeadline, extra_config = ExtraConfig
}) ->
StateList = [{id, Id}, {name, Name}, {description, Desc},
{client_id, ClientId}, {client_secret, ClientSecret},
{config_endpoint, ConfEp}, {lasttime_updated, LastTimeUpdated},
{ready, Ready}, {local_endpoint, LocalEndpoint}, {keys, Keys},
{request_scopes, Scopes}, {issuer, Issuer},
{meta_data, MetaData}, {config_deadline, ConfDeadline},
{extra_config, ExtraConfig}
],
maps:merge(Config, maps:from_list(StateList)).
handle_config(Data, Header, #state{issuer=Issuer} = State) ->
Config = decode_json(Data),
ConfIssuer = maps:get(issuer, Config, undefined),
SameIssuer = is_same_issuer(ConfIssuer, Issuer),
AuthCodeFlow = supports_auth_code(Config),
case {SameIssuer, AuthCodeFlow} of
{true, true} ->
Deadline = header_to_deadline(Header),
trigger_registration(),
State#state{config = Config, issuer=ConfIssuer,
request_id=undefined, config_deadline=Deadline};
{true, false} ->
Error = no_authcode_support,
State#state{error = Error, ready=false, request_id=undefined};
_ ->
Deadline = deadline_in(600),
Error = {bad_issuer_config, Issuer, ConfIssuer, Data},
State#state{error = Error, ready=false, request_id=undefined,
config_deadline=Deadline}
end.
supports_auth_code(#{response_types_supported := ResponseTypes} = Config) ->
Code = <<"code">>,
AuthCode = <<"authorization_code">>,
GrantTypes = maps:get(grant_types_supported, Config,
[AuthCode, <<"implicit">>]),
CodeResponse = lists:member(Code, ResponseTypes),
AuthGrant = lists:member(AuthCode, GrantTypes),
CodeResponse and AuthGrant;
supports_auth_code(_) ->
false.
header_to_deadline(Header) ->
Cache = lists:keyfind(<<"cache-control">>, 1, Header),
Delta =
try
cache_deadline(Cache)
catch _:_ ->
3600
end,
deadline_in(Delta).
cache_deadline({_, Cache}) ->
Entries = binary:split(Cache, [<<",">>, <<"=">>, <<" ">>],
[global, trim_all]),
MaxAge = fun(Entry, true) ->
binary_to_integer(Entry);
(<<"max-age">>, _) ->
true;
(_, Res) ->
Res
end,
lists:foldl(MaxAge, false, Entries).
deadline_in(Seconds) ->
timestamp() + Seconds.
handle_keys(Data, _Header, State) ->
%TODO: maybe also implement a keys deadline
KeyConfig=decode_json(Data),
KeyList = maps:get(keys, KeyConfig, []),
NewState = State#state{keys = KeyList, lasttime_updated = timestamp(),
request_id = undefined, key_requests = []},
send_key_replies(KeyList, State),
case length(KeyList) > 0 of
true ->
NewState;
false ->
NewState#state{error = {no_keys, Data}}
end.
send_key_replies(Keys, #state{key_requests = Requests}) ->
Send = fun(From, _) ->
gen_server:reply(From, {ok, Keys})
end,
lists:foldl(Send, ok, Requests).
handle_registration(Data, _Header, State) ->
%TODO: implement update at expire data/time or retrieval when needed
MetaData=decode_json(Data),
ClientId = maps:get(client_id, MetaData, undefined),
ClientSecret = maps:get(client_secret, MetaData, undefined),
ClientSecretExpire = maps:get(client_secret_expires_at, MetaData,
undefined),
case is_binary(ClientId) and is_binary(ClientSecret)
and is_number(ClientSecretExpire) of
true ->
State#state{meta_data = MetaData, client_id = ClientId,
client_secret = ClientSecret, ready = true,
lasttime_updated = timestamp(), request_id = undefined};
false ->
State#state{error=no_clientid, meta_data=MetaData, ready = false,
client_id=undefined, client_secret = undefined,
request_id = undefined}
end.
decode_json(Data) ->
try
jsone:decode(Data, [{keys, attempt_atom}, {object_format, map}])
catch error:badarg ->
#{}
end.
handle_http_client_crash(Reason, #state{config_tries=Tries,
retrieving=Type} = State) ->
MaxRetries = application:get_env(oidcc, provider_max_tries, 5),
case Tries >= MaxRetries of
true ->
State#state{error = Reason};
false ->
case Type of
keys ->
trigger_key_retrieval();
config ->
trigger_config_retrieval()
end,
State#state{request_id=undefined,
http_result={}, config_tries=Tries+1,
config_deadline = deadline_in(300)}
end.
trigger_config_retrieval() ->
gen_server:cast(self(), retrieve_config).
trigger_config_retrieval_if_needed(#state{config_deadline=Deadline} = State)
when is_integer(Deadline) ->
Soon = timestamp() + ?TIMEOUT,
case Soon >= Deadline of
true ->
trigger_config_retrieval(),
{ok, State#state{config_deadline = undefined}};
_ ->
{ok, State}
end;
trigger_config_retrieval_if_needed(State) ->
{ok, State}.
trigger_key_retrieval() ->
gen_server:cast(self(), retrieve_keys).
trigger_registration() ->
gen_server:cast(self(), register_if_needed).
timestamp() ->
erlang:system_time(seconds).
to_config_endpoint(IssuerOrEndpoint) ->
Slash = <<"/">>,
Config = <<".well-known/openid-configuration">>,
ConfigS = << Slash/binary, Config/binary >>,
Pos = byte_size(IssuerOrEndpoint) - 33,
case binary:match(IssuerOrEndpoint, ConfigS) of
{Pos, 33} ->
Endpoint = IssuerOrEndpoint,
Endpoint;
_ ->
Issuer = IssuerOrEndpoint,
case binary:last(Issuer) of
$/ -> << Issuer/binary, Config/binary>>;
_ -> << Issuer/binary, ConfigS/binary>>
end
end.
config_ep_to_issuer(ConfigEp) ->
[Issuer] = binary:split(ConfigEp, [<<"/.well-known/openid-configuration">>],
[trim_all, global]),
Issuer.
is_same_issuer(Config, Issuer) ->
Slash = <<"/">>,
IssuerSlash = << Issuer/binary, Slash/binary >>,
(Config =:= Issuer) or (Config =:= IssuerSlash).