1: %%============================================================================== 2: %% Copyright 2015 Erlang Solutions Ltd. 3: %% 4: %% Licensed under the Apache License, Version 2.0 (the "License"); 5: %% you may not use this file except in compliance with the License. 6: %% You may obtain a copy of the License at 7: %% 8: %% http://www.apache.org/licenses/LICENSE-2.0 9: %% 10: %% Unless required by applicable law or agreed to in writing, software 11: %% distributed under the License is distributed on an "AS IS" BASIS, 12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13: %% See the License for the specific language governing permissions and 14: %% limitations under the License. 15: %%============================================================================== 16: 17: -module(oauth_SUITE). 18: -compile([export_all, nowarn_export_all]). 19: 20: -include_lib("escalus/include/escalus.hrl"). 21: -include_lib("escalus/include/escalus_xmlns.hrl"). 22: -include_lib("common_test/include/ct.hrl"). 23: -include_lib("exml/include/exml.hrl"). 24: 25: -import(distributed_helper, [mim/0, 26: require_rpc_nodes/1, 27: rpc/4]). 28: 29: -import(domain_helper, [domain/0]). 30: 31: %%-------------------------------------------------------------------- 32: %% Suite configuration 33: %%-------------------------------------------------------------------- 34: 35: all() -> 36: [ 37: {group, token_login}, 38: {group, token_revocation}, 39: {group, provision_token}, 40: {group, cleanup}, 41: {group, sasl_mechanisms} 42: ]. 43: 44: groups() -> 45: G = [ 46: {token_login, [sequence], token_login_tests()}, 47: {token_revocation, [sequence], token_revocation_tests()}, 48: {provision_token, [], [provision_token_login]}, 49: {cleanup, [], [token_removed_on_user_removal]}, 50: {sasl_mechanisms, [], [check_for_oauth_with_mod_auth_token_not_loaded, 51: check_for_oauth_with_mod_auth_token_loaded]} 52: ], 53: ct_helper:repeat_all_until_all_ok(G). 54: 55: token_login_tests() -> 56: [ 57: disco_test, 58: request_tokens_test, 59: login_access_token_test, 60: login_refresh_token_test, 61: login_with_other_users_token, 62: login_with_malformed_token 63: ]. 64: 65: token_revocation_tests() -> 66: [ 67: login_with_revoked_token_test, 68: token_revocation_test 69: ]. 70: 71: suite() -> 72: require_rpc_nodes([mim]) ++ escalus:suite(). 73: 74: %%-------------------------------------------------------------------- 75: %% Init & teardown 76: %%-------------------------------------------------------------------- 77: 78: init_per_suite(Config0) -> 79: case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of 80: true -> 81: HostType = domain_helper:host_type(), 82: Config = dynamic_modules:save_modules(HostType, Config0), 83: dynamic_modules:ensure_modules(HostType, required_modules()), 84: escalus:init_per_suite(Config); 85: false -> 86: {skip, "RDBMS not available"} 87: end. 88: 89: end_per_suite(Config) -> 90: dynamic_modules:restore_modules(Config), 91: escalus:end_per_suite(Config). 92: 93: init_per_group(GroupName, Config) -> 94: AuthOpts = mongoose_helper:auth_opts_with_password_format(password_format(GroupName)), 95: HostType = domain_helper:host_type(), 96: Config1 = mongoose_helper:backup_and_set_config_option(Config, {auth, HostType}, AuthOpts), 97: Config2 = escalus:create_users(Config1, escalus:get_users([bob, alice])), 98: assert_password_format(GroupName, Config2). 99: 100: password_format(login_scram) -> scram; 101: password_format(_) -> plain. 102: 103: end_per_group(cleanup, Config) -> 104: mongoose_helper:restore_config(Config), 105: escalus:delete_users(Config, escalus:get_users([alice])); 106: end_per_group(_GroupName, Config) -> 107: mongoose_helper:restore_config(Config), 108: escalus:delete_users(Config, escalus:get_users([bob, alice])). 109: 110: init_per_testcase(check_for_oauth_with_mod_auth_token_not_loaded, Config) -> 111: HostType = domain_helper:host_type(), 112: dynamic_modules:stop(HostType, mod_auth_token), 113: init_per_testcase(generic, Config); 114: init_per_testcase(CaseName, Config) -> 115: clean_token_db(), 116: escalus:init_per_testcase(CaseName, Config). 117: 118: 119: end_per_testcase(check_for_oauth_with_mod_auth_token_not_loaded, Config) -> 120: HostType = domain_helper:host_type(), 121: dynamic_modules:start(HostType, mod_auth_token, auth_token_opts()), 122: end_per_testcase(generic, Config); 123: end_per_testcase(CaseName, Config) -> 124: clean_token_db(), 125: escalus:end_per_testcase(CaseName, Config). 126: 127: 128: %% 129: %% Tests 130: %% 131: 132: disco_test(Config) -> 133: escalus:story( 134: Config, [{alice, 1}], 135: fun(Alice) -> 136: escalus_client:send(Alice, escalus_stanza:disco_info(domain())), 137: Response = escalus_client:wait_for_stanza(Alice), 138: escalus:assert(has_feature, [?NS_ESL_TOKEN_AUTH], Response) 139: end). 140: 141: request_tokens_test(Config) -> 142: request_tokens_once_logged_in_impl(Config, bob). 143: 144: login_with_revoked_token_test(Config) -> 145: %% given 146: RevokedToken = get_revoked_token(Config, bob), 147: token_login_failure(Config, bob, RevokedToken). 148: 149: token_login_failure(Config, User, Token) -> 150: %% when 151: Result = login_with_token(Config, User, Token), 152: % then 153: {{auth_failed, _}, _} = Result. 154: 155: get_revoked_token(Config, UserName) -> 156: BJID = escalus_users:get_jid(Config, UserName), 157: JID = jid:from_binary(BJID), 158: HostType = domain_helper:host_type(), 159: Token = rpc(mim(), mod_auth_token, token, [HostType, JID, refresh]), 160: ValidSeqNo = rpc(mim(), mod_auth_token_rdbms, get_valid_sequence_number, [HostType, JID]), 161: RevokedToken0 = record_set(Token, [{5, invalid_sequence_no(ValidSeqNo)}, 162: {7, undefined}, 163: {8, undefined}]), 164: RevokedToken = rpc(mim(), mod_auth_token, token_with_mac, [HostType, RevokedToken0]), 165: rpc(mim(), mod_auth_token, serialize, [RevokedToken]). 166: 167: invalid_sequence_no(SeqNo) -> 168: SeqNo - 1. 169: 170: request_tokens_once_logged_in(Config) -> 171: request_tokens_once_logged_in_impl(Config, bob). 172: 173: request_tokens_once_logged_in_impl(Config, User) -> 174: Self = self(), 175: Ref = make_ref(), 176: Fun = fun(Client) -> 177: ClientShortJid = escalus_utils:get_short_jid(Client), 178: R = escalus_stanza:query_el(?NS_ESL_TOKEN_AUTH, []), 179: IQ = escalus_stanza:iq(ClientShortJid, <<"get">>, [R]), 180: escalus:send(Client, IQ), 181: Result = escalus:wait_for_stanza(Client), 182: {AT, RT} = extract_tokens(Result), 183: Self ! {tokens, Ref, {AT, RT}} 184: end, 185: escalus:story(Config, [{User, 1}], Fun), 186: receive 187: {tokens, Ref, Tokens} -> 188: Tokens 189: after 190: 1000 -> error 191: end. 192: 193: login_access_token_test(Config) -> 194: Tokens = request_tokens_once_logged_in_impl(Config, bob), 195: login_access_token_impl(Config, Tokens). 196: 197: login_refresh_token_test(Config) -> 198: Tokens = request_tokens_once_logged_in_impl(Config, bob), 199: login_refresh_token_impl(Config, Tokens). 200: 201: %% Scenario describing JID spoofing with an eavesdropped / stolen token. 202: login_with_other_users_token(Config) -> 203: %% given user and another user's token 204: {_, BobsToken} = request_tokens_once_logged_in_impl(Config, bob), 205: AliceSpec = user_authenticating_with_token(Config, alice, BobsToken), 206: %% when we try to log in 207: ConnSteps = [start_stream, 208: stream_features, 209: maybe_use_ssl, 210: authenticate, 211: fun (Alice = #client{props = Props}, Features) -> 212: escalus:send(Alice, escalus_stanza:bind(<<"test-resource">>)), 213: BindReply = escalus_connection:get_stanza(Alice, bind_reply), 214: {Alice#client{props = [{bind_reply, BindReply} | Props]}, Features} 215: end], 216: {ok, #client{props = Props}, _} = escalus_connection:start(AliceSpec, ConnSteps), 217: %% then the server recognizes us as the other user 218: LoggedInAs = extract_bound_jid(proplists:get_value(bind_reply, Props)), 219: true = escalus_utils:get_username(LoggedInAs) /= escalus_users:get_username(Config, AliceSpec). 220: 221: login_with_malformed_token(Config) -> 222: %% given 223: MalformedToken = <<"malformed ", (crypto:strong_rand_bytes(64))/bytes>>, 224: %% when / then 225: token_login_failure(Config, bob, MalformedToken). 226: 227: login_refresh_token_impl(Config, {_AccessToken, RefreshToken}) -> 228: BobSpec = escalus_users:get_userspec(Config, bob), 229: 230: ConnSteps = [start_stream, 231: stream_features, 232: maybe_use_ssl, 233: maybe_use_compression 234: ], 235: 236: {ok, ClientConnection = #client{props = Props}, _Features} = escalus_connection:start(BobSpec, ConnSteps), 237: Props2 = lists:keystore(oauth_token, 1, Props, {oauth_token, RefreshToken}), 238: (catch escalus_auth:auth_sasl_oauth(ClientConnection, Props2)), 239: ok. 240: 241: %% users logs in using access token he obtained in previous session (stream has been 242: %% already reset) 243: login_access_token_impl(Config, {AccessToken, _RefreshToken}) -> 244: {{ok, Props}, ClientConnection} = login_with_token(Config, bob, AccessToken), 245: escalus_connection:reset_parser(ClientConnection), 246: ClientConn1 = escalus_session:start_stream(ClientConnection#client{props = Props}), 247: {ClientConn1, _} = escalus_session:stream_features(ClientConn1, []), 248: %todo: create step out of above lines 249: ClientConn2 = escalus_session:bind(ClientConn1), 250: ClientConn3 = escalus_session:session(ClientConn2), 251: escalus:send(ClientConn3, escalus_stanza:presence(<<"available">>)), 252: escalus:assert(is_presence, escalus:wait_for_stanza(ClientConn3)). 253: 254: login_with_token(Config, User, Token) -> 255: UserSpec = escalus_users:get_userspec(Config, User), 256: ConnSteps = [start_stream, 257: stream_features, 258: maybe_use_ssl, 259: maybe_use_compression], 260: {ok, ClientConnection = #client{props = Props}, _Features} = escalus_connection:start(UserSpec, ConnSteps), 261: Props2 = lists:keystore(oauth_token, 1, Props, {oauth_token, Token}), 262: AuthResult = (catch escalus_auth:auth_sasl_oauth(ClientConnection, Props2)), 263: {AuthResult, ClientConnection}. 264: 265: token_revocation_test(Config) -> 266: %% given 267: {Owner, _SeqNoToRevoke, Token} = get_owner_seqno_to_revoke(Config, bob), 268: %% when 269: ok = revoke_token(Owner), 270: %% then 271: token_login_failure(Config, bob, Token). 272: 273: get_owner_seqno_to_revoke(Config, User) -> 274: {_, RefreshToken} = request_tokens_once_logged_in_impl(Config, User), 275: [_, BOwner, _, SeqNo, _] = binary:split(RefreshToken, <<0>>, [global]), 276: Owner = jid:from_binary(BOwner), 277: {Owner, binary_to_integer(SeqNo), RefreshToken}. 278: 279: revoke_token(Owner) -> 280: rpc(mim(), mod_auth_token, revoke, [domain_helper:host_type(), Owner]). 281: 282: token_removed_on_user_removal(Config) -> 283: %% given existing user with token and XMPP (de)registration available 284: _Tokens = request_tokens_once_logged_in_impl(Config, bob), 285: true = is_xmpp_registration_available(domain_helper:host_type()), 286: %% when user account is deleted 287: S = fun (Bob) -> 288: IQ = escalus_stanza:remove_account(), 289: escalus:send(Bob, IQ), 290: escalus:assert(is_iq_result, [IQ], escalus:wait_for_stanza(Bob)) 291: end, 292: escalus:story(Config, [{bob, 1}], S), 293: %% then token database doesn't contain user's tokens (cleanup is done after IQ result) 294: mongoose_helper:wait_until(fun() -> get_users_token(Config, bob) end, {selected, []}). 295: 296: provision_token_login(Config) -> 297: %% given 298: VCard = make_vcard(Config, bob), 299: ProvisionToken = make_provision_token(Config, bob, VCard), 300: UserSpec = user_authenticating_with_token(Config, bob, ProvisionToken), 301: %% when logging in with provision token 302: {ok, Conn, _} = escalus_connection:start(UserSpec), 303: escalus:send(Conn, escalus_stanza:vcard_request()), 304: %% then user's vcard is placed into the database on login 305: Result = escalus:wait_for_stanza(Conn), 306: VCard = exml_query:subelement(Result, <<"vCard">>). 307: 308: 309: check_for_oauth_with_mod_auth_token_not_loaded(Config) -> 310: AliceSpec = escalus_users:get_userspec(Config, alice), 311: ConnSteps = [start_stream, 312: stream_features, 313: maybe_use_ssl, 314: maybe_use_compression], 315: {ok, _, Features} = escalus_connection:start(AliceSpec, ConnSteps), 316: false = lists:member(<<"X-OAUTH">>, proplists:get_value(sasl_mechanisms, 317: Features, [])). 318: 319: check_for_oauth_with_mod_auth_token_loaded(Config) -> 320: AliceSpec = escalus_users:get_userspec(Config, alice), 321: ConnSteps = [start_stream, 322: stream_features, 323: maybe_use_ssl, 324: maybe_use_compression], 325: {ok, _, Features} = escalus_connection:start(AliceSpec, ConnSteps), 326: true = lists:member(<<"X-OAUTH">>, proplists:get_value(sasl_mechanisms, 327: Features, [])). 328: 329: 330: %% 331: %% Helpers 332: %% 333: 334: extract_tokens(#xmlel{name = <<"iq">>, children = [#xmlel{name = <<"items">>} = Items ]}) -> 335: ATD = exml_query:path(Items, [{element, <<"access_token">>}, cdata]), 336: RTD = exml_query:path(Items, [{element, <<"refresh_token">>}, cdata]), 337: {base64:decode(ATD), base64:decode(RTD)}. 338: 339: assert_password_format(GroupName, Config) -> 340: Users = proplists:get_value(escalus_users, Config), 341: [verify_format(GroupName, User) || User <- Users], 342: Config. 343: 344: verify_format(GroupName, {_User, Props}) -> 345: Username = escalus_utils:jid_to_lower(proplists:get_value(username, Props)), 346: Server = proplists:get_value(server, Props), 347: Password = proplists:get_value(password, Props), 348: JID = mongoose_helper:make_jid(Username, Server), 349: {SPassword, _} = rpc(mim(), ejabberd_auth, get_passterm_with_authmodule, 350: [domain_helper:host_type(), JID]), 351: do_verify_format(GroupName, Password, SPassword). 352: 353: do_verify_format(login_scram, _Password, SPassword) -> 354: %% returned password is a tuple containing scram data 355: {_, _, _, _} = SPassword; 356: do_verify_format(_, Password, SPassword) -> 357: Password = SPassword. 358: 359: %% @doc Set Fields of the Record to Values, 360: %% when {Field, Value} <- FieldValues (in list comprehension syntax). 361: record_set(Record, FieldValues) -> 362: F = fun({Field, Value}, Rec) -> 363: setelement(Field, Rec, Value) 364: end, 365: lists:foldl(F, Record, FieldValues). 366: 367: mimctl(Config, CmdAndArgs) -> 368: Node = ct:get_config({hosts, mim, node}), 369: ejabberd_node_utils:call_ctl_with_args(Node, convert_args(CmdAndArgs), Config). 370: 371: convert_args(Args) -> [ convert_arg(A) || A <- Args ]. 372: 373: convert_arg(B) when is_binary(B) -> binary_to_list(B); 374: convert_arg(A) when is_atom(A) -> atom_to_list(A); 375: convert_arg(S) when is_list(S) -> S. 376: 377: clean_token_db() -> 378: Q = [<<"DELETE FROM auth_token">>], 379: {updated, _} = rpc(mim(), mongoose_rdbms, sql_query, [domain_helper:host_type(), Q]). 380: 381: get_users_token(C, User) -> 382: Q = ["SELECT * FROM auth_token at " 383: "WHERE at.owner = '", to_lower(escalus_users:get_jid(C, User)), "';"], 384: rpc(mim(), mongoose_rdbms, sql_query, [escalus_users:get_server(C, User), Q]). 385: 386: is_xmpp_registration_available(Domain) -> 387: rpc(mim(), gen_mod, is_loaded, [Domain, mod_register]). 388: 389: user_authenticating_with_token(Config, UserName, Token) -> 390: Spec1 = lists:keystore(oauth_token, 1, escalus_users:get_userspec(Config, UserName), 391: {oauth_token, Token}), 392: lists:keystore(auth, 1, Spec1, {auth, {escalus_auth, auth_sasl_oauth}}). 393: 394: extract_bound_jid(BindReply) -> 395: exml_query:path(BindReply, [{element, <<"bind">>}, {element, <<"jid">>}, 396: cdata]). 397: 398: get_provision_key(Domain) -> 399: RPCArgs = [Domain, provision_pre_shared], 400: [{_, RawKey}] = rpc(mim(), mongoose_hooks, get_key, RPCArgs), 401: RawKey. 402: 403: make_vcard(Config, User) -> 404: T = <<"<vCard xmlns='vcard-temp'>" 405: "<FN>Full Name</FN>" 406: "<NICKNAME>{{nick}}</NICKNAME>" 407: "</vCard>">>, 408: escalus_stanza:from_template(T, [{nick, escalus_users:get_username(Config, User)}]). 409: 410: make_provision_token(Config, User, VCard) -> 411: ExpiryFarInTheFuture = {{2055, 10, 27}, {10, 54, 22}}, 412: Username = escalus_users:get_username(Config, User), 413: Domain = escalus_users:get_server(Config, User), 414: ServerSideJID = jid:make(Username, Domain, <<>>), 415: T0 = {token, provision, 416: ExpiryFarInTheFuture, 417: ServerSideJID, 418: %% sequence no 419: undefined, 420: VCard, 421: %% MAC 422: undefined, 423: %% body 424: undefined}, 425: T = rpc(mim(), mod_auth_token, token_with_mac, [domain_helper:host_type(), T0]), 426: %% assert no RPC error occured 427: {token, provision} = {element(1, T), element(2, T)}, 428: serialize(T). 429: 430: serialize(ServerSideToken) -> 431: Serialized = rpc(mim(), mod_auth_token, serialize, [ServerSideToken]), 432: case is_binary(Serialized) of 433: true -> Serialized; 434: false -> error(Serialized) 435: end. 436: 437: to_lower(B) when is_binary(B) -> 438: list_to_binary(string:to_lower(binary_to_list(B))). 439: 440: required_modules() -> 441: KeyOpts = #{backend => ct_helper:get_internal_database(), 442: keys => #{token_secret => ram, 443: %% This is a hack for tests! As the name implies, 444: %% a pre-shared key should be read from a file stored 445: %% on disk. This way it can be shared with trusted 3rd 446: %% parties who can use it to sign tokens for users 447: %% to authenticate with and MongooseIM to verify. 448: provision_pre_shared => ram}}, 449: KeyStoreOpts = config_parser_helper:mod_config(mod_keystore, KeyOpts), 450: [{mod_last, stopped}, 451: {mod_keystore, KeyStoreOpts}, 452: {mod_auth_token, auth_token_opts()}]. 453: 454: auth_token_opts() -> 455: Defaults = config_parser_helper:default_mod_config(mod_auth_token), 456: Defaults#{validity_period => #{access => #{value => 60, unit => minutes}, 457: refresh => #{value => 1, unit => days}}}.