Current section

Files

Jump to
oidcc src oidcc.erl
Raw

src/oidcc.erl

-module(oidcc).
-export([add_openid_provider/2]).
-export([add_openid_provider/3]).
-export([find_openid_provider/1]).
-export([get_openid_provider_info/1]).
-export([get_openid_provider_list/0]).
-export([create_redirect_url/1]).
-export([create_redirect_url/2]).
-export([create_redirect_for_session/1]).
-export([retrieve_and_validate_token/2]).
-export([retrieve_and_validate_token/3]).
-export([retrieve_user_info/2]).
-export([retrieve_user_info/3]).
-export([retrieve_fresh_token/2]).
-export([retrieve_fresh_token/3]).
-export([introspect_token/2]).
-export([register_module/1]).
%% @doc
%% add an OpenID Connect Provider to the list of possible Providers
%%
%% this automatically triggers the fetching of the configuration endpoint
%% @end
-spec add_openid_provider(binary(), binary()) -> {ok, Id::binary(), Pid::pid()}.
add_openid_provider(IssuerOrConfigEP, LocalEndpoint) ->
add_openid_provider(IssuerOrConfigEP, LocalEndpoint, #{}).
-spec add_openid_provider(binary(), binary(), map()) ->
{ok, Id::binary(), Pid::pid()} |
{error, Reason::any()}.
add_openid_provider(IssuerOrConfigEP, LocalEndpoint, AdditionalConfig) ->
BasicConfig = #{name => <<"OpenId Connect Provider">>,
description => <<"a minimal configured provider">>,
client_id => undefined,
client_secret => <<"">>,
request_scopes => undefined,
registration_params => #{}
},
ForceUpdate = #{ issuer_or_endpoint => IssuerOrConfigEP,
local_endpoint => LocalEndpoint},
ConfigBase = maps:merge(BasicConfig, AdditionalConfig),
Config = maps:merge(ConfigBase, ForceUpdate),
oidcc_openid_provider_mgr:add_openid_provider(Config).
-spec find_openid_provider(Issuer::binary()) -> {ok, pid()}
| {error, not_found}.
find_openid_provider(Issuer) ->
oidcc_openid_provider_mgr:find_openid_provider(Issuer).
%% @doc
%% get information from a given OpenId Connect Provider
%%
%% the parameter can either be the Pid or it's Id. The result is a map
%% containing all the information gathered by connecting to the configuration
%% endpoint given at the beginning.
%% the map also contains a boolean flag 'ready' which is true, once the
%% configuration has been fetched.
%% @end
-spec get_openid_provider_info(pid() | binary()) -> {ok, map()}.
get_openid_provider_info(Pid) when is_pid(Pid) ->
oidcc_openid_provider:get_config(Pid);
get_openid_provider_info(OpenIdProviderId) when is_binary(OpenIdProviderId) ->
case oidcc_openid_provider_mgr:get_openid_provider(OpenIdProviderId) of
{ok, Pid} ->
oidcc_openid_provider:get_config(Pid);
{error, Reason} ->
{error, Reason}
end.
%% @doc
%% get a list of all currently configured OpenId Connect Provider
%%
%% it is a list of tuples {Id, Pid}
%% @end
-spec get_openid_provider_list() -> {ok, [{binary(), pid()}]}.
get_openid_provider_list() ->
oidcc_openid_provider_mgr:get_openid_provider_list().
%% @doc
%% same as create_redirect_url/4 but with all parameters being fetched
%% from the given session, except the provider
%% @end
-spec create_redirect_for_session(pid()) -> {ok, binary()}.
create_redirect_for_session(Session) ->
{ok, Scopes} = oidcc_session:get_scopes(Session),
{ok, State} = oidcc_session:get_id(Session),
{ok, Nonce} = oidcc_session:get_nonce(Session),
{ok, Pkce} = oidcc_session:get_pkce(Session),
{ok, OpenIdProviderId} = oidcc_session:get_provider(Session),
Config = #{scopes => Scopes, state => State, nonce => Nonce, pkce => Pkce},
create_redirect_url(OpenIdProviderId, Config).
%% @doc
%% same as create_redirect_url/4 but with State and Nonce being undefined and
%% scope being openid
%% @end
-spec create_redirect_url(binary()) ->
{ok, binary()} | {error, provider_not_ready}.
create_redirect_url(OpenIdProviderId) ->
create_redirect_url(OpenIdProviderId, #{}).
%% @doc
%% same as create_redirect_url/4 but with State and Nonce being undefined
%% @end
-spec create_redirect_url(binary(), map()) ->
{ok, binary()} | {error, provider_not_ready}.
create_redirect_url(OpenIdProviderId, Config) ->
BasicConfig = #{
scopes => [openid],
state => undefined,
nonce => undefined,
pkce => undefined
},
{ok, Info} = get_openid_provider_info(OpenIdProviderId),
create_redirect_url_if_ready(Info, maps:merge(BasicConfig, Config)).
%% @doc
%% retrieve the token using the authcode received before and directly validate
%% the result.
%%
%% the authcode was sent to the local endpoint by the OpenId Connect provider,
%% using redirects. the result is textual representation of the token and should
%% be verified using parse_and_validate_token/3
%% @end
retrieve_and_validate_token(AuthCode, ProviderId) ->
retrieve_and_validate_token(AuthCode, ProviderId, #{}).
retrieve_and_validate_token(AuthCode, ProviderId, Config) ->
Pkce = maps:get(pkce, Config, undefined),
Nonce = maps:get(nonce, Config, undefined),
Scopes = scopes_to_bin(maps:get(scope, Config, []), <<>>),
{ok, Info} = get_openid_provider_info(ProviderId),
#{local_endpoint := LocalEndpoint} = Info,
QsBody = [ {<<"grant_type">>, <<"authorization_code">>},
{<<"code">>, AuthCode},
{<<"redirect_uri">>, LocalEndpoint}
],
case retrieve_a_token(QsBody, Pkce, Info) of
{ok, Token} ->
TokenMap = oidcc_token:extract_token_map(Token, Scopes),
oidcc_token:validate_token_map(TokenMap, ProviderId, Nonce, true);
Error -> Error
end.
%% @doc
%% retrieve the informations of a user given by its token map or an access token
%%
%% this is done by looking up the UserInfoEndpoint from the configuration and
%% then requesting info, using the access token as bearer token
%% @end
-spec retrieve_user_info(map() | binary(), binary()) ->
{ok, map()} | {error, any()}.
retrieve_user_info(#{access := _, id := _, refresh := _} = TokenMap,
ProviderIdOrPid) ->
Subject = extract_subject(TokenMap),
retrieve_user_info(TokenMap, ProviderIdOrPid, Subject);
retrieve_user_info(AccessToken, ProviderIdOrPid) when is_binary(AccessToken) ->
retrieve_user_info(AccessToken, ProviderIdOrPid, undefined);
retrieve_user_info(_, _) ->
{error, bad_token}.
-spec retrieve_user_info(Token, ProviderIdOrPid, Subject)-> {ok, map()} |
{error, any()} when
Token :: binary() | map(),
ProviderIdOrPid :: binary() | pid(),
Subject :: binary() | undefined.
retrieve_user_info(Token, ProviderIdOrPid, Subject) ->
{ok,
#{userinfo_endpoint := Endpoint}
} = get_openid_provider_info(ProviderIdOrPid),
AccessToken = extract_access_token(Token),
Header = [bearer_auth(AccessToken)],
HttpResult = oidcc_http_util:sync_http(get, Endpoint, Header, true),
return_validated_user_info(HttpResult, Subject).
retrieve_fresh_token(RefreshToken, OpenIdProvider) ->
retrieve_fresh_token(RefreshToken, [], OpenIdProvider).
retrieve_fresh_token(RefreshToken, Scopes, OpenIdProvider) ->
{ok, Config} = get_openid_provider_info(OpenIdProvider),
BodyQs0 = [
{<<"refresh_token">>, RefreshToken},
{<<"grant_type">>, <<"refresh_token">>}
],
BodyQs = append_scope(Scopes, BodyQs0),
retrieve_a_token(BodyQs, Config).
%% @doc
%% introspect the given token at the given provider
%%
%% this is done by looking up the IntrospectionEndpoint from the configuration
%% and then requesting info, using the client credentials as authentication
%% @end
-spec introspect_token(Token, ProviderOrConfig) -> {ok, map()} |
{error, any()} when
Token :: binary() | map(),
ProviderOrConfig :: binary() | map().
introspect_token(TokenMapIn, #{introspection_endpoint := Endpoint,
client_id := ClientId,
client_secret := ClientSecret}) ->
AccessToken = extract_access_token(TokenMapIn),
Header = [
{"accept", "application/json"},
basic_auth(ClientId, ClientSecret)
],
BodyQs = oidcc_http_util:qs([{<<"token">>, AccessToken}]),
HttpResult = oidcc_http_util:sync_http(post, Endpoint, Header,
"application/x-www-form-urlencoded",
BodyQs, true),
case return_token(HttpResult) of
{ok, Token} ->
TokenMap = oidcc_token:introspect_token_map(Token, ClientId),
{ok, TokenMap};
Error -> Error
end;
introspect_token(Token, ProviderId) ->
{ok, Config} = get_openid_provider_info(ProviderId),
introspect_token(Token, Config).
register_module(Module) ->
oidcc_client:register(Module).
retrieve_a_token(QsBodyIn, OpenIdProviderInfo) ->
retrieve_a_token(QsBodyIn, undefined, OpenIdProviderInfo).
retrieve_a_token(QsBodyIn, Pkce, OpenIdProviderInfo) ->
#{ client_id := ClientId,
client_secret := Secret,
token_endpoint := Endpoint
} = OpenIdProviderInfo,
AuthMethods = maps:get(token_endpoint_auth_methods_supported,
OpenIdProviderInfo, [<<"client_secret_basic">>]),
AuthMethod = select_preferred_auth(AuthMethods),
Header0 = [],
{QsBody, Header} = add_authentication_code_verifier(QsBodyIn, Header0,
AuthMethod, ClientId,
Secret, Pkce),
Body = oidcc_http_util:qs(QsBody),
return_token(oidcc_http_util:sync_http(post, Endpoint, Header,
"application/x-www-form-urlencoded",
Body)).
extract_subject(#{sub := Subject}) ->
Subject;
extract_subject(#{id := IdToken}) ->
extract_subject(IdToken);
extract_subject(#{claims := Claims}) ->
extract_subject(Claims);
extract_subject(_) ->
undefined.
extract_access_token(#{access := AccessToken}) ->
#{token := Token} = AccessToken,
Token;
extract_access_token(#{token := Token}) ->
Token;
extract_access_token(Token) when is_binary(Token) ->
Token.
create_redirect_url_if_ready(#{ready := false}, _) ->
{error, provider_not_ready};
create_redirect_url_if_ready(Info, Config) ->
#{ local_endpoint := LocalEndpoint,
client_id := ClientId,
authorization_endpoint := AuthEndpoint
} = Info,
#{
scopes := Scopes,
state := OidcState,
nonce := OidcNonce,
pkce := Pkce
} = Config,
UrlList = [
{<<"response_type">>, <<"code">>},
{<<"client_id">>, ClientId},
{<<"redirect_uri">>, LocalEndpoint}
],
UrlList1 = append_state(OidcState, UrlList),
UrlList2 = append_nonce(OidcNonce, UrlList1),
UrlList3 = append_code_challenge(Pkce, UrlList2),
UrlList4 = append_scope(Scopes, UrlList3),
Qs = oidcc_http_util:qs(UrlList4),
Url = << AuthEndpoint/binary, <<"?">>/binary, Qs/binary>>,
{ok, Url}.
append_scope(<<>>, QsList) ->
QsList;
append_scope(Scope, QsList) when is_binary(Scope) ->
[{<<"scope">>, Scope} | QsList];
append_scope(Scopes, QsList) when is_list(Scopes) ->
append_scope(scopes_to_bin(Scopes, <<>>), QsList).
append_state(State, UrlList) when is_binary(State) ->
[{<<"state">>, State} | UrlList];
append_state(_, UrlList) ->
UrlList.
append_nonce(Nonce, UrlList) when is_binary(Nonce) ->
[{<<"nonce">>, Nonce} | UrlList];
append_nonce(_, UrlList) ->
UrlList.
append_code_challenge(#{challenge := Challenge} = Pkce, UrlList) ->
NewUrlList = [{<<"code_challenge">>, Challenge} | UrlList],
append_code_challenge_method(Pkce, NewUrlList);
append_code_challenge(_, UrlList) ->
UrlList.
append_code_challenge_method(#{method := 'S256'}, UrlList) ->
[{<<"code_challenge_method">>, <<"S256">>} | UrlList];
append_code_challenge_method(_, UrlList) ->
[{<<"code_challenge_method">>, <<"plain">>} | UrlList].
select_preferred_auth(AuthMethodsSupported) ->
Selector = fun(Method, Current) ->
case {Method, Current} of
{_, basic} -> basic;
{<<"client_secret_basic">>, _} -> basic;
{<<"client_secret_post">>, _} -> post;
{_, Current} -> Current
end
end,
lists:foldl(Selector, undefined, AuthMethodsSupported).
add_authentication_code_verifier(QsBodyList, Header, basic, ClientId, Secret,
undefined) ->
NewHeader = [basic_auth(ClientId, Secret)| Header ],
{QsBodyList, NewHeader};
add_authentication_code_verifier(QsBodyList, Header, post, ClientId,
ClientSecret, undefined) ->
NewBodyList = [ {<<"client_id">>, ClientId},
{<<"client_secret">>, ClientSecret} | QsBodyList ],
{NewBodyList, Header};
add_authentication_code_verifier(B, H, undefined, CI, CS, undefined) ->
add_authentication_code_verifier(B, H, basic, CI, CS, undefined);
add_authentication_code_verifier(BodyQs, Header, AuthMethod, CI, CS,
#{verifier:=CV}) ->
BodyQs1 = [{<<"code_verifier">>, CV} | BodyQs],
add_authentication_code_verifier(BodyQs1, Header, AuthMethod, CI, CS,
undefined).
return_token( {ok, #{body := Token, status := 200}} ) ->
{ok, Token};
return_token( {ok, #{body := Body, status := Status}} ) ->
{error, {http_error, Status, Body}}.
return_validated_user_info(HttpData, undefined) ->
return_json_info(HttpData);
return_validated_user_info(HttpData, Subject) ->
case return_json_info(HttpData) of
{ok, #{ sub := Subject } = Map} -> {ok, Map};
{ok, _} -> {error, bad_subject};
Other -> Other
end.
return_json_info({ok, #{status := 200, body := Data}}) ->
try jsone:decode(Data, [{keys, attempt_atom}, {object_format, map}])
of Map -> {ok, Map}
catch Error -> {error, Error}
end;
return_json_info({ok, Map}) ->
{error, {bad_status, Map}}.
basic_auth(User, Secret) ->
UserEnc = oidcc_http_util:urlencode(User),
SecretEnc = oidcc_http_util:urlencode(Secret),
RawAuth = <<UserEnc/binary, <<":">>/binary, SecretEnc/binary>>,
AuthData = base64:encode(RawAuth),
BasicAuth = << <<"Basic ">>/binary, AuthData/binary >>,
{<<"authorization">>, BasicAuth}.
bearer_auth(Token) ->
{<<"authorization">>, << <<"Bearer ">>/binary, Token/binary >>}.
scopes_to_bin([], Bin) ->
Bin;
scopes_to_bin([H | T], <<>>) when is_binary(H) ->
scopes_to_bin(T, H);
scopes_to_bin([H | T], Bin) when is_binary(H) ->
NewBin = << H/binary, <<" ">>/binary, Bin/binary>>,
scopes_to_bin(T, NewBin);
scopes_to_bin([H | T], Bin) when is_atom(H) ->
List = [ atom_to_binary(H, utf8) | T],
scopes_to_bin(List, Bin);
scopes_to_bin([H | T], Bin) when is_list(H) ->
List = [ list_to_binary(H) | T],
scopes_to_bin(List, Bin).