Packages
oidcc
1.4.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.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).