├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── build.config ├── erlang.mk ├── rabbitmq-components.mk ├── src ├── rabbit_auth_backend_oauth.erl ├── rabbit_auth_backend_oauth_app.erl ├── rabbit_oauth2_access_token.erl ├── rabbit_oauth2_auth.erl ├── rabbit_oauth2_backend.erl ├── rabbit_oauth2_scope.erl ├── rabbit_oauth2_storage.erl └── rabbitmq_auth_backend_oauth.app.src ├── templates └── auth_form.dtl └── test └── src ├── mock_transport.erl ├── rabbit_oauth2_auth_test.erl └── rabbit_oauth2_backend_test.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .sw? 2 | .*.sw? 3 | *.beam 4 | /.erlang.mk/ 5 | /cover/ 6 | /deps/ 7 | /doc/ 8 | /ebin/ 9 | /logs/ 10 | /plugins/ 11 | 12 | /rabbitmq_auth_backend_oauth.d 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: erlang 3 | notifications: 4 | email: 5 | - alerts@rabbitmq.com 6 | addons: 7 | apt: 8 | packages: 9 | - xsltproc 10 | otp_release: 11 | - "R16B03-1" 12 | - "17.5" 13 | - "18.0" 14 | 15 | # The checkout made by Travis is a "detached HEAD". We switch back 16 | # to a tag or a branch. This pleases our git_rmq fetch method in 17 | # rabbitmq-components.mk and the proper tag/branch is selected in 18 | # dependencies too. 19 | before_script: (test "$TRAVIS_TAG" && git checkout "$TRAVIS_TAG") || (test "$TRAVIS_BRANCH" && git checkout "$TRAVIS_BRANCH") 20 | 21 | script: make tests 22 | 23 | cache: 24 | apt: true 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = rabbitmq_auth_backend_oauth 2 | 3 | DEPS = cowboy rabbitmq_web_dispatch rabbit_common oauth2 erlydtl 4 | TEST_DEPS = rabbit 5 | 6 | DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk 7 | 8 | # FIXME: Use erlang.mk patched for RabbitMQ, while waiting for PRs to be 9 | # reviewed and merged. 10 | 11 | ERLANG_MK_REPO = https://github.com/rabbitmq/erlang.mk.git 12 | ERLANG_MK_COMMIT = rabbitmq-tmp 13 | 14 | include rabbitmq-components.mk 15 | include erlang.mk 16 | 17 | 18 | WITH_BROKER_TEST_COMMANDS:= \ 19 | rabbit_oauth2_backend_test:broker_tests() \ 20 | rabbit_oauth2_auth_test:tests() 21 | 22 | STANDALONE_TEST_COMMANDS := \ 23 | rabbit_oauth2_backend_test:standalone_tests() 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ OAuth 2.0 Authorization Backend. 2 | 3 | This plugin aims to provide OAuth 2.0 authorization for RabbitMQ clients. 4 | 5 | ## Project Maturity 6 | 7 | This project ws **a spike that's no longer under development**. See [rabbitmq-auth-backend-oauth2](https://github.com/rabbitmq/rabbitmq-auth-backend-oauth2) for an OAuth 2/JWT [authentication and authorisation backend](https://www.rabbitmq.com/access-control.html) for RabbitMQ. 8 | 9 | ## Auth workflow 10 | 11 | ### Token grant. 12 | 13 | Internal: 14 | 15 | Client use some grant to request `access_token` in some `scope`. 16 | 17 | Token is being created with scope and expire after some time. 18 | Client can also be issued `refresh_token` to refresh `access_token`. 19 | 20 | If client use user credentioals, user access permissions to `scope` is being checked. 21 | 22 | External: 23 | 24 | External auth server sends request to token handler to create `access_token` with scope and expiry. 25 | 26 | ### Client access. 27 | 28 | Client connects to RabbitMQ using `access_token` as username and will have access to resources based on `scope` 29 | 30 | 31 | ## Components 32 | 33 | This module contin following parts: 34 | 35 | 1. Rabbit auth backend `rabbit_auth_backend_oauth.erl`. Module to authorize clients with `access_token` used as username. 36 | 2. Oauth backend (yeah, also backend) `rabbit_oauth2_backend.erl`. Module to work with OAuth2 clients and tokens, direct them to mnesia storage, manage scopes. (https://github.com/kivra/oauth2/blob/master/src/oauth2_backend.erl) 37 | 3. OAuth2 http server `rabbit_oauth2_auth.erl`. Cowboy handler to grant access codes and tokens. Has no references to rabbitmq and works with oauth library only. Can be made separate plugin. 38 | 4. Token endpoint for external Auth server `rabbit_oauth2_access_token.erl`. Accepts requests like `{"acess_token":..., "scope":..., "expires_in":..., "created_at":...}` and creates `access_token` record in DB. Can be used by external authorization server to issue tokens for rabbitmq. 39 | 40 | Endpoint is configured by application env `auth_server`, which can be `{internal, Conf}` or `{external, Conf}`. To set up internal (`rabbit_oauth2_auth`) or external (`rabbit_oauth2_access_token`) auth server. 41 | 42 | Grant and client types are managed by authorization server handler only. 43 | 44 | ## Scopes 45 | 46 | *Scopes is discussion topic, because current implementation provide not enough flexibility.* 47 | 48 | To define `access_token` access to specific VHost or resource OAuth2 scopes are used. 49 | Scope can be a set of strings. Each element in scope define access to specific resource permission. 50 | 51 | Format of scope element: `___`, where 52 | 53 | - `` - vhost of recource 54 | - `` can be `q` - queue, `ex` - exchange, or `t` - topic 55 | - `` - access permission (configure, read, write) 56 | - `` - resource name (exact, no regexps allowed) 57 | 58 | When granting `access_code` to scope on behalf of some user scope is checked to be available to this user. For this purpose another `auth_backend` is used. `rabbit_oauth2_backend.erl` currently contains constant `rabbit_auth_backend_internal`, can be configurable. 59 | 60 | As you can see, scope syntax restrict some vhosts and it is not easy to support regex resource names, because granting regex scope to regex user permissions will require solving regex inclusion problem (which is not so easy) 61 | 62 | -------------------------------------------------------------------------------- /build.config: -------------------------------------------------------------------------------- 1 | # Do *not* comment or remove core modules 2 | # unless you know what you are doing. 3 | # 4 | # Feel free to comment plugins out however. 5 | 6 | # Core modules. 7 | core/core 8 | index/* 9 | core/index 10 | core/deps 11 | 12 | # Plugins that must run before Erlang code gets compiled. 13 | plugins/erlydtl 14 | plugins/protobuffs 15 | 16 | # Core modules, continued. 17 | core/erlc 18 | core/docs 19 | core/rel 20 | core/test 21 | core/compat 22 | 23 | # Plugins. 24 | plugins/asciidoc 25 | plugins/bootstrap 26 | plugins/c_src 27 | plugins/ci 28 | plugins/ct 29 | plugins/dialyzer 30 | plugins/edoc 31 | plugins/elvis 32 | plugins/escript 33 | # plugins/eunit 34 | plugins/relx 35 | plugins/shell 36 | plugins/triq 37 | plugins/xref 38 | 39 | # Plugins enhancing the functionality of other plugins. 40 | plugins/cover 41 | 42 | # Core modules which can use variables from plugins. 43 | core/deps-tools 44 | -------------------------------------------------------------------------------- /rabbitmq-components.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(.DEFAULT_GOAL),) 2 | # Define default goal to `all` because this file defines some targets 3 | # before the inclusion of erlang.mk leading to the wrong target becoming 4 | # the default. 5 | .DEFAULT_GOAL = all 6 | endif 7 | 8 | # Automatically add rabbitmq-common to the dependencies, at least for 9 | # the Makefiles. 10 | ifneq ($(PROJECT),rabbit_common) 11 | ifneq ($(PROJECT),rabbitmq_public_umbrella) 12 | ifeq ($(filter rabbit_common,$(DEPS)),) 13 | DEPS += rabbit_common 14 | endif 15 | endif 16 | endif 17 | 18 | # -------------------------------------------------------------------- 19 | # RabbitMQ components. 20 | # -------------------------------------------------------------------- 21 | 22 | # For RabbitMQ repositories, we want to checkout branches which match 23 | # the parent project. For instance, if the parent project is on a 24 | # release tag, dependencies must be on the same release tag. If the 25 | # parent project is on a topic branch, dependencies must be on the same 26 | # topic branch or fallback to `stable` or `master` whichever was the 27 | # base of the topic branch. 28 | 29 | dep_amqp_client = git_rmq rabbitmq-erlang-client $(current_rmq_ref) $(base_rmq_ref) master 30 | dep_rabbit = git_rmq rabbitmq-server $(current_rmq_ref) $(base_rmq_ref) master 31 | dep_rabbit_common = git_rmq rabbitmq-common $(current_rmq_ref) $(base_rmq_ref) master 32 | dep_rabbitmq_amqp1_0 = git_rmq rabbitmq-amqp1.0 $(current_rmq_ref) $(base_rmq_ref) master 33 | dep_rabbitmq_auth_backend_amqp = git_rmq rabbitmq-auth-backend-amqp $(current_rmq_ref) $(base_rmq_ref) master 34 | dep_rabbitmq_auth_backend_http = git_rmq rabbitmq-auth-backend-http $(current_rmq_ref) $(base_rmq_ref) master 35 | dep_rabbitmq_auth_backend_ldap = git_rmq rabbitmq-auth-backend-ldap $(current_rmq_ref) $(base_rmq_ref) master 36 | dep_rabbitmq_auth_mechanism_ssl = git_rmq rabbitmq-auth-mechanism-ssl $(current_rmq_ref) $(base_rmq_ref) master 37 | dep_rabbitmq_boot_steps_visualiser = git_rmq rabbitmq-boot-steps-visualiser $(current_rmq_ref) $(base_rmq_ref) master 38 | dep_rabbitmq_clusterer = git_rmq rabbitmq-clusterer $(current_rmq_ref) $(base_rmq_ref) master 39 | dep_rabbitmq_codegen = git_rmq rabbitmq-codegen $(current_rmq_ref) $(base_rmq_ref) master 40 | dep_rabbitmq_consistent_hash_exchange = git_rmq rabbitmq-consistent-hash-exchange $(current_rmq_ref) $(base_rmq_ref) master 41 | dep_rabbitmq_delayed_message_exchange = git_rmq rabbitmq-delayed-message-exchange $(current_rmq_ref) $(base_rmq_ref) master 42 | dep_rabbitmq_dotnet_client = git_rmq rabbitmq-dotnet-client $(current_rmq_ref) $(base_rmq_ref) master 43 | dep_rabbitmq_event_exchange = git_rmq rabbitmq-event-exchange $(current_rmq_ref) $(base_rmq_ref) master 44 | dep_rabbitmq_federation = git_rmq rabbitmq-federation $(current_rmq_ref) $(base_rmq_ref) master 45 | dep_rabbitmq_federation_management = git_rmq rabbitmq-federation-management $(current_rmq_ref) $(base_rmq_ref) master 46 | dep_rabbitmq_java_client = git_rmq rabbitmq-java-client $(current_rmq_ref) $(base_rmq_ref) master 47 | dep_rabbitmq_lvc = git_rmq rabbitmq-lvc-plugin $(current_rmq_ref) $(base_rmq_ref) master 48 | dep_rabbitmq_management = git_rmq rabbitmq-management $(current_rmq_ref) $(base_rmq_ref) master 49 | dep_rabbitmq_management_agent = git_rmq rabbitmq-management-agent $(current_rmq_ref) $(base_rmq_ref) master 50 | dep_rabbitmq_management_exchange = git_rmq rabbitmq-management-exchange $(current_rmq_ref) $(base_rmq_ref) master 51 | dep_rabbitmq_management_themes = git_rmq rabbitmq-management-themes $(current_rmq_ref) $(base_rmq_ref) master 52 | dep_rabbitmq_management_visualiser = git_rmq rabbitmq-management-visualiser $(current_rmq_ref) $(base_rmq_ref) master 53 | dep_rabbitmq_message_timestamp = git_rmq rabbitmq-message-timestamp $(current_rmq_ref) $(base_rmq_ref) master 54 | dep_rabbitmq_metronome = git_rmq rabbitmq-metronome $(current_rmq_ref) $(base_rmq_ref) master 55 | dep_rabbitmq_mqtt = git_rmq rabbitmq-mqtt $(current_rmq_ref) $(base_rmq_ref) master 56 | dep_rabbitmq_recent_history_exchange = git_rmq rabbitmq-recent-history-exchange $(current_rmq_ref) $(base_rmq_ref) master 57 | dep_rabbitmq_rtopic_exchange = git_rmq rabbitmq-rtopic-exchange $(current_rmq_ref) $(base_rmq_ref) master 58 | dep_rabbitmq_sharding = git_rmq rabbitmq-sharding $(current_rmq_ref) $(base_rmq_ref) master 59 | dep_rabbitmq_shovel = git_rmq rabbitmq-shovel $(current_rmq_ref) $(base_rmq_ref) master 60 | dep_rabbitmq_shovel_management = git_rmq rabbitmq-shovel-management $(current_rmq_ref) $(base_rmq_ref) master 61 | dep_rabbitmq_stomp = git_rmq rabbitmq-stomp $(current_rmq_ref) $(base_rmq_ref) master 62 | dep_rabbitmq_toke = git_rmq rabbitmq-toke $(current_rmq_ref) $(base_rmq_ref) master 63 | dep_rabbitmq_top = git_rmq rabbitmq-top $(current_rmq_ref) $(base_rmq_ref) master 64 | dep_rabbitmq_tracing = git_rmq rabbitmq-tracing $(current_rmq_ref) $(base_rmq_ref) master 65 | dep_rabbitmq_test = git_rmq rabbitmq-test $(current_rmq_ref) $(base_rmq_ref) master 66 | dep_rabbitmq_web_dispatch = git_rmq rabbitmq-web-dispatch $(current_rmq_ref) $(base_rmq_ref) master 67 | dep_rabbitmq_web_stomp = git_rmq rabbitmq-web-stomp $(current_rmq_ref) $(base_rmq_ref) master 68 | dep_rabbitmq_web_stomp_examples = git_rmq rabbitmq-web-stomp-examples $(current_rmq_ref) $(base_rmq_ref) master 69 | dep_rabbitmq_website = git_rmq rabbitmq-website $(current_rmq_ref) $(base_rmq_ref) live master 70 | dep_sockjs = git_rmq sockjs-erlang $(current_rmq_ref) $(base_rmq_ref) master 71 | dep_toke = git_rmq toke $(current_rmq_ref) $(base_rmq_ref) master 72 | 73 | dep_rabbitmq_public_umbrella = git_rmq rabbitmq-public-umbrella $(current_rmq_ref) $(base_rmq_ref) master 74 | 75 | # FIXME: As of 2015-11-20, we depend on Ranch 1.2.1, but erlang.mk 76 | # defaults to Ranch 1.1.0. All projects depending indirectly on Ranch 77 | # needs to add "ranch" as a BUILD_DEPS. The list of projects needing 78 | # this workaround are: 79 | # o rabbitmq-web-stomp 80 | dep_ranch = git https://github.com/ninenines/ranch 1.2.1 81 | 82 | RABBITMQ_COMPONENTS = amqp_client \ 83 | rabbit \ 84 | rabbit_common \ 85 | rabbitmq_amqp1_0 \ 86 | rabbitmq_auth_backend_amqp \ 87 | rabbitmq_auth_backend_http \ 88 | rabbitmq_auth_backend_ldap \ 89 | rabbitmq_auth_mechanism_ssl \ 90 | rabbitmq_boot_steps_visualiser \ 91 | rabbitmq_clusterer \ 92 | rabbitmq_codegen \ 93 | rabbitmq_consistent_hash_exchange \ 94 | rabbitmq_delayed_message_exchange \ 95 | rabbitmq_dotnet_client \ 96 | rabbitmq_event_exchange \ 97 | rabbitmq_federation \ 98 | rabbitmq_federation_management \ 99 | rabbitmq_java_client \ 100 | rabbitmq_lvc \ 101 | rabbitmq_management \ 102 | rabbitmq_management_agent \ 103 | rabbitmq_management_exchange \ 104 | rabbitmq_management_themes \ 105 | rabbitmq_management_visualiser \ 106 | rabbitmq_message_timestamp \ 107 | rabbitmq_metronome \ 108 | rabbitmq_mqtt \ 109 | rabbitmq_recent_history_exchange \ 110 | rabbitmq_rtopic_exchange \ 111 | rabbitmq_sharding \ 112 | rabbitmq_shovel \ 113 | rabbitmq_shovel_management \ 114 | rabbitmq_stomp \ 115 | rabbitmq_test \ 116 | rabbitmq_toke \ 117 | rabbitmq_top \ 118 | rabbitmq_tracing \ 119 | rabbitmq_web_dispatch \ 120 | rabbitmq_web_stomp \ 121 | rabbitmq_web_stomp_examples \ 122 | rabbitmq_website 123 | 124 | # Several components have a custom erlang.mk/build.config, mainly 125 | # to disable eunit. Therefore, we can't use the top-level project's 126 | # erlang.mk copy. 127 | NO_AUTOPATCH += $(RABBITMQ_COMPONENTS) 128 | 129 | ifeq ($(origin current_rmq_ref),undefined) 130 | ifneq ($(wildcard .git),) 131 | current_rmq_ref := $(shell (\ 132 | ref=$$(git branch --list | awk '/^\* \(.*detached / {ref=$$0; sub(/.*detached [^ ]+ /, "", ref); sub(/\)$$/, "", ref); print ref; exit;} /^\* / {ref=$$0; sub(/^\* /, "", ref); print ref; exit}');\ 133 | if test "$$(git rev-parse --short HEAD)" != "$$ref"; then echo "$$ref"; fi)) 134 | else 135 | current_rmq_ref := master 136 | endif 137 | endif 138 | export current_rmq_ref 139 | 140 | ifeq ($(origin base_rmq_ref),undefined) 141 | ifneq ($(wildcard .git),) 142 | base_rmq_ref := $(shell \ 143 | (git rev-parse --verify -q stable >/dev/null && \ 144 | git merge-base --is-ancestor $$(git merge-base master HEAD) stable && \ 145 | echo stable) || \ 146 | echo master) 147 | else 148 | base_rmq_ref := master 149 | endif 150 | endif 151 | export base_rmq_ref 152 | 153 | # Repository URL selection. 154 | # 155 | # First, we infer other components' location from the current project 156 | # repository URL, if it's a Git repository: 157 | # - We take the "origin" remote URL as the base 158 | # - The current project name and repository name is replaced by the 159 | # target's properties: 160 | # eg. rabbitmq-common is replaced by rabbitmq-codegen 161 | # eg. rabbit_common is replaced by rabbitmq_codegen 162 | # 163 | # If cloning from this computed location fails, we fallback to RabbitMQ 164 | # upstream which is GitHub. 165 | 166 | # Maccro to transform eg. "rabbit_common" to "rabbitmq-common". 167 | rmq_cmp_repo_name = $(word 2,$(dep_$(1))) 168 | 169 | # Upstream URL for the current project. 170 | RABBITMQ_COMPONENT_REPO_NAME := $(call rmq_cmp_repo_name,$(PROJECT)) 171 | RABBITMQ_UPSTREAM_FETCH_URL ?= https://github.com/rabbitmq/$(RABBITMQ_COMPONENT_REPO_NAME).git 172 | RABBITMQ_UPSTREAM_PUSH_URL ?= git@github.com:rabbitmq/$(RABBITMQ_COMPONENT_REPO_NAME).git 173 | 174 | # Current URL for the current project. If this is not a Git clone, 175 | # default to the upstream Git repository. 176 | ifneq ($(wildcard .git),) 177 | git_origin_fetch_url := $(shell git config remote.origin.url) 178 | git_origin_push_url := $(shell git config remote.origin.pushurl || git config remote.origin.url) 179 | RABBITMQ_CURRENT_FETCH_URL ?= $(git_origin_fetch_url) 180 | RABBITMQ_CURRENT_PUSH_URL ?= $(git_origin_push_url) 181 | else 182 | RABBITMQ_CURRENT_FETCH_URL ?= $(RABBITMQ_UPSTREAM_FETCH_URL) 183 | RABBITMQ_CURRENT_PUSH_URL ?= $(RABBITMQ_UPSTREAM_PUSH_URL) 184 | endif 185 | 186 | # Macro to replace the following pattern: 187 | # 1. /foo.git -> /bar.git 188 | # 2. /foo -> /bar 189 | # 3. /foo/ -> /bar/ 190 | subst_repo_name = $(patsubst %/$(1)/%,%/$(2)/%,$(patsubst %/$(1),%/$(2),$(patsubst %/$(1).git,%/$(2).git,$(3)))) 191 | 192 | # Macro to replace both the project's name (eg. "rabbit_common") and 193 | # repository name (eg. "rabbitmq-common") by the target's equivalent. 194 | # 195 | # This macro is kept on one line because we don't want whitespaces in 196 | # the returned value, as it's used in $(dep_fetch_git_rmq) in a shell 197 | # single-quoted string. 198 | dep_rmq_repo = $(if $(dep_$(2)),$(call subst_repo_name,$(PROJECT),$(2),$(call subst_repo_name,$(RABBITMQ_COMPONENT_REPO_NAME),$(call rmq_cmp_repo_name,$(2)),$(1))),$(pkg_$(1)_repo)) 199 | 200 | dep_rmq_commits = $(if $(dep_$(1)), \ 201 | $(wordlist 3,$(words $(dep_$(1))),$(dep_$(1))), \ 202 | $(pkg_$(1)_commit)) 203 | 204 | define dep_fetch_git_rmq 205 | fetch_url1='$(call dep_rmq_repo,$(RABBITMQ_CURRENT_FETCH_URL),$(1))'; \ 206 | fetch_url2='$(call dep_rmq_repo,$(RABBITMQ_UPSTREAM_FETCH_URL),$(1))'; \ 207 | if test "$$$$fetch_url1" != '$(RABBITMQ_CURRENT_FETCH_URL)' && \ 208 | git clone -q -n -- "$$$$fetch_url1" $(DEPS_DIR)/$(call dep_name,$(1)); then \ 209 | fetch_url="$$$$fetch_url1"; \ 210 | push_url='$(call dep_rmq_repo,$(RABBITMQ_CURRENT_PUSH_URL),$(1))'; \ 211 | elif git clone -q -n -- "$$$$fetch_url2" $(DEPS_DIR)/$(call dep_name,$(1)); then \ 212 | fetch_url="$$$$fetch_url2"; \ 213 | push_url='$(call dep_rmq_repo,$(RABBITMQ_UPSTREAM_PUSH_URL),$(1))'; \ 214 | fi; \ 215 | cd $(DEPS_DIR)/$(call dep_name,$(1)) && ( \ 216 | $(foreach ref,$(call dep_rmq_commits,$(1)), \ 217 | git checkout -q $(ref) >/dev/null 2>&1 || \ 218 | ) \ 219 | (echo "error: no valid pathspec among: $(call dep_rmq_commits,$(1))" \ 220 | 1>&2 && false) ) && \ 221 | (test "$$$$fetch_url" = "$$$$push_url" || \ 222 | git remote set-url --push origin "$$$$push_url") 223 | endef 224 | 225 | # -------------------------------------------------------------------- 226 | # Component distribution. 227 | # -------------------------------------------------------------------- 228 | 229 | list-dist-deps:: 230 | @: 231 | 232 | prepare-dist:: 233 | @: 234 | 235 | # -------------------------------------------------------------------- 236 | # Run a RabbitMQ node (moved from rabbitmq-run.mk as a workaround). 237 | # -------------------------------------------------------------------- 238 | 239 | # Add "rabbit" to the build dependencies when the user wants to start 240 | # a broker or to the test dependencies when the user wants to test a 241 | # project. 242 | # 243 | # NOTE: This should belong to rabbitmq-run.mk. Unfortunately, it is 244 | # loaded *after* erlang.mk which is too late to add a dependency. That's 245 | # why rabbitmq-components.mk knows the list of targets which start a 246 | # broker and add "rabbit" to the dependencies in this case. 247 | 248 | ifneq ($(PROJECT),rabbit) 249 | ifeq ($(filter rabbit,$(DEPS) $(BUILD_DEPS)),) 250 | RUN_RMQ_TARGETS = run-broker \ 251 | run-background-broker \ 252 | run-node \ 253 | run-background-node \ 254 | start-background-node 255 | 256 | ifneq ($(filter $(RUN_RMQ_TARGETS),$(MAKECMDGOALS)),) 257 | BUILD_DEPS += rabbit 258 | endif 259 | endif 260 | 261 | ifeq ($(filter rabbit,$(DEPS) $(BUILD_DEPS) $(TEST_DEPS)),) 262 | ifneq ($(filter check tests tests-with-broker test,$(MAKECMDGOALS)),) 263 | TEST_DEPS += rabbit 264 | endif 265 | endif 266 | endif 267 | 268 | ifeq ($(filter rabbit_public_umbrella amqp_client rabbit_common rabbitmq_test,$(PROJECT)),) 269 | ifeq ($(filter rabbitmq_test,$(DEPS) $(BUILD_DEPS) $(TEST_DEPS)),) 270 | TEST_DEPS += rabbitmq_test 271 | endif 272 | endif 273 | 274 | # -------------------------------------------------------------------- 275 | # rabbitmq-components.mk checks. 276 | # -------------------------------------------------------------------- 277 | 278 | ifeq ($(PROJECT),rabbit_common) 279 | else ifdef SKIP_RMQCOMP_CHECK 280 | else ifeq ($(IS_DEP),1) 281 | else ifneq ($(filter co up,$(MAKECMDGOALS)),) 282 | else 283 | # In all other cases, rabbitmq-components.mk must be in sync. 284 | deps:: check-rabbitmq-components.mk 285 | fetch-deps: check-rabbitmq-components.mk 286 | endif 287 | 288 | # If this project is under the Umbrella project, we override $(DEPS_DIR) 289 | # to point to the Umbrella's one. We also disable `make distclean` so 290 | # $(DEPS_DIR) is not accidentally removed. 291 | 292 | ifneq ($(wildcard ../../UMBRELLA.md),) 293 | UNDER_UMBRELLA = 1 294 | else ifneq ($(wildcard UMBRELLA.md),) 295 | UNDER_UMBRELLA = 1 296 | endif 297 | 298 | ifeq ($(UNDER_UMBRELLA),1) 299 | ifneq ($(PROJECT),rabbitmq_public_umbrella) 300 | DEPS_DIR ?= $(abspath ..) 301 | 302 | distclean:: distclean-components 303 | @: 304 | 305 | distclean-components: 306 | endif 307 | 308 | ifneq ($(filter distclean distclean-deps,$(MAKECMDGOALS)),) 309 | SKIP_DEPS = 1 310 | endif 311 | endif 312 | 313 | UPSTREAM_RMQ_COMPONENTS_MK = $(DEPS_DIR)/rabbit_common/mk/rabbitmq-components.mk 314 | 315 | check-rabbitmq-components.mk: 316 | $(verbose) cmp -s rabbitmq-components.mk \ 317 | $(UPSTREAM_RMQ_COMPONENTS_MK) || \ 318 | (echo "error: rabbitmq-components.mk must be updated!" 1>&2; \ 319 | false) 320 | 321 | ifeq ($(PROJECT),rabbit_common) 322 | rabbitmq-components-mk: 323 | @: 324 | else 325 | rabbitmq-components-mk: 326 | $(gen_verbose) cp -a $(UPSTREAM_RMQ_COMPONENTS_MK) . 327 | ifeq ($(DO_COMMIT),yes) 328 | $(verbose) git diff --quiet rabbitmq-components.mk \ 329 | || git commit -m 'Update rabbitmq-components.mk' rabbitmq-components.mk 330 | endif 331 | endif 332 | -------------------------------------------------------------------------------- /src/rabbit_auth_backend_oauth.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2007-2015 Pivotal Software, Inc. All rights reserved. 15 | 16 | %% 17 | %% This is backend for OAuth 2.0 authorization. 18 | %% It Allows to use Rabbitmq with OAuth authorization servers. 19 | %% Authorization token is used as Username. 20 | 21 | -module(rabbit_auth_backend_oauth). 22 | 23 | -include_lib("rabbit_common/include/rabbit.hrl"). 24 | 25 | -behaviour(rabbit_authn_backend). 26 | -behaviour(rabbit_authz_backend). 27 | 28 | -export([description/0]). 29 | -export([user_login_authentication/2, user_login_authorization/1, 30 | check_vhost_access/3, check_resource_access/3]). 31 | 32 | -rabbit_boot_step({rabbit_auth_backend_oauth_mnesia, 33 | [{description, "authosation oauth2: mnesia"}, 34 | {mfa, {rabbit_oauth2_storage, setup_schema, []}}, 35 | {requires, database}, 36 | {enables, external_infrastructure}]}). 37 | 38 | -rabbit_boot_step({rabbit_auth_backend_oauth_backend_env, 39 | [{description, "authosation oauth2: oauth2 backend"}, 40 | {mfa, {rabbit_oauth2_backend, oauth2_backend_env, []}}, 41 | {requires, pre_boot}, 42 | {enables, kernel_ready}]}). 43 | 44 | %%-------------------------------------------------------------------- 45 | 46 | description() -> 47 | [{name, <<"OAUTH">>}, 48 | {description, <<"OAUTH authentication / authorisation">>}]. 49 | 50 | %%-------------------------------------------------------------------- 51 | 52 | user_login_authentication(Token, _AuthProps) -> 53 | case oauth2:verify_access_token(Token, []) of 54 | {ok, _} -> {ok, #auth_user{ username = Token, tags = [], impl = none}}; 55 | {error, access_denied} -> {refused, "token ~p rejected", [Token]} 56 | end. 57 | 58 | user_login_authorization(Username) -> 59 | case user_login_authentication(Username, []) of 60 | {ok, #auth_user{impl = Impl, tags = Tags}} -> {ok, Impl, Tags}; 61 | Else -> Else 62 | end. 63 | 64 | check_vhost_access(#auth_user{username = Username}, VHost, _Sock) -> 65 | with_token_context(Username, fun(Ctx) -> 66 | rabbit_oauth2_scope:vhost_access(VHost, Ctx) 67 | end). 68 | 69 | check_resource_access(#auth_user{username = Username}, Resource, Permission) -> 70 | with_token_context(Username, fun(Ctx) -> 71 | rabbit_oauth2_scope:resource_access(Resource, Permission, Ctx) 72 | end). 73 | 74 | with_token_context(Token, Fun) -> 75 | case oauth2:verify_access_token(Token, []) of 76 | {ok, {_, TokenCtx}} -> Fun(TokenCtx); 77 | {error, access_denied} -> false 78 | end. 79 | 80 | -------------------------------------------------------------------------------- /src/rabbit_auth_backend_oauth_app.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_auth_backend_oauth_app). 2 | 3 | 4 | -behaviour(application). 5 | -export([start/2, stop/1, reset_dispatcher/1]). 6 | 7 | -behaviour(supervisor). 8 | -export([init/1]). 9 | 10 | -define(CONTEXT, rabbit_oauth). 11 | 12 | start(_Type, _StartArgs) -> 13 | {ok, AuthServer} = application:get_env(rabbitmq_auth_backend_oauth, 14 | auth_server), 15 | maybe_register_context(AuthServer, []), 16 | supervisor:start_link({local,?MODULE},?MODULE,[]). 17 | 18 | stop(_State) -> 19 | unregister_context(), 20 | ok. 21 | 22 | %% At the point at which this is invoked we have both newly enabled 23 | %% apps and about-to-disable apps running (so that 24 | %% rabbit_mgmt_reset_handler can look at all of them to find 25 | %% extensions). Therefore we have to explicitly exclude 26 | %% about-to-disable apps from our new dispatcher. 27 | reset_dispatcher(IgnoreApps) -> 28 | unregister_context(), 29 | {ok, AuthServer} = application:get_env(rabbitmq_auth_backend_oauth, 30 | auth_server), 31 | maybe_register_context(AuthServer, IgnoreApps). 32 | 33 | maybe_register_context(undefined, _IgnoreApps) -> ok; 34 | maybe_register_context([], _IgnoreApps) -> ok; 35 | maybe_register_context({AuthServerType, Listener}, _IgnoreApps) -> 36 | {Route, Description} = case AuthServerType of 37 | internal -> 38 | {[{'_', [{"/oauth", rabbit_oauth2_auth, []}]}], 39 | "RabbitMQ Oauth2 auth server"}; 40 | external -> 41 | {[{'_', [{"/access_token", rabbit_oauth2_access_token, []}]}], 42 | "RabbitMQ Oauth2 access token endpoint"} 43 | end, 44 | % TODO log 45 | rabbit_web_dispatch:register_context_handler( 46 | ?CONTEXT, Listener, "", 47 | cowboy_router:compile(Route), Description). 48 | 49 | unregister_context() -> 50 | rabbit_web_dispatch:unregister_context(?CONTEXT). 51 | 52 | init([]) -> 53 | {ok, {{one_for_one,3,10},[]}}. 54 | -------------------------------------------------------------------------------- /src/rabbit_oauth2_access_token.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_oauth2_access_token). 2 | 3 | -export([ 4 | init/3 5 | ,rest_init/2 6 | ,allowed_methods/2 7 | ]). 8 | 9 | -export([content_types_accepted/2]). 10 | 11 | -export([process_post/2]). 12 | 13 | 14 | %%%=================================================================== 15 | %%% Cowboy callbacks 16 | %%%=================================================================== 17 | 18 | init(_Transport, _Req, _Opts) -> 19 | %% Compile the DTL template used for the authentication 20 | %% form in the implicit grant flow. 21 | {upgrade, protocol, cowboy_rest}. 22 | 23 | rest_init(Req, _Opts) -> 24 | {ok, Req, undefined_state}. 25 | 26 | content_types_accepted(Req, State) -> 27 | {[{{<<"application">>, <<"json">>, []}, process_post}, 28 | {{<<"application">>, <<"x-www-form-urlencoded">>, []}, process_post}], 29 | Req, State}. 30 | 31 | allowed_methods(Req, State) -> 32 | {[<<"POST">>], Req, State}. 33 | 34 | process_post(Req, State) -> 35 | {ok, Params, Req2} = cowboy_req:body_qs(Req), 36 | Token = proplists:get_value(<<"access_token">>, Params), 37 | Scope = binary:split(proplists:get_value(<<"scope">>, Params), 38 | <<" ">>, [global]), 39 | ExpiresIn = binary_to_integer(proplists:get_value(<<"expires_in">>, 40 | Params)), 41 | CreatedAt = proplists:get_value(<<"created_at">>, Params, 42 | time_compat:os_system_time(seconds)), 43 | % TODO: default scope 44 | {ok, Reply} = case Token == undefined 45 | orelse Scope == undefined 46 | orelse ExpiresIn == undefined of 47 | true -> cowboy_req:reply(400, [], <<"Bad Request.">>, Req2); 48 | false -> 49 | ok = rabbit_oauth2_backend:add_access_token(Token, 50 | Scope, 51 | ExpiresIn, 52 | CreatedAt), 53 | cowboy_req:reply(200, [], <<"Ok">>, Req2) 54 | end, 55 | {halt, Reply, State}. -------------------------------------------------------------------------------- /src/rabbit_oauth2_auth.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_oauth2_auth). 2 | 3 | -export([ 4 | init/3 5 | ,rest_init/2 6 | ,allowed_methods/2 7 | ]). 8 | 9 | -export([ 10 | content_types_provided/2 11 | ,content_types_accepted/2 12 | ]). 13 | 14 | -export([ 15 | process_post/2 16 | ,process_get/2 17 | ]). 18 | 19 | -export([binary_join/2]). 20 | 21 | %%%=================================================================== 22 | %%% Cowboy callbacks 23 | %%%=================================================================== 24 | 25 | init(_Transport, _Req, _Opts) -> 26 | %% Compile the DTL template used for the authentication 27 | %% form in the implicit grant flow. 28 | {upgrade, protocol, cowboy_rest}. 29 | 30 | rest_init(Req, _Opts) -> 31 | {ok, Req, undefined_state}. 32 | 33 | content_types_provided(Req, State) -> 34 | {[{{<<"text">>, <<"html">>, []}, process_get}], Req, State}. 35 | 36 | content_types_accepted(Req, State) -> 37 | {[{{<<"application">>, <<"json">>, []}, process_post}, 38 | {{<<"application">>, <<"x-www-form-urlencoded">>, []}, process_post}], 39 | Req, State}. 40 | 41 | allowed_methods(Req, State) -> 42 | {[<<"POST">>, <<"GET">>], Req, State}. 43 | 44 | process_post(Req, State) -> 45 | {ok, Params, Req2} = cowboy_req:body_qs(Req), 46 | {ok, Reply} = 47 | case proplists:get_value(<<"grant_type">>, Params) of 48 | <<"password">> -> 49 | process_password_grant(Req2, Params); 50 | <<"client_credentials">> -> 51 | process_client_credentials_grant(Req2, Params); 52 | <<"authorization_code">> -> 53 | process_authorization_token_grant(Req2, Params); 54 | undefined -> 55 | case proplists:get_value(<<"response_type">>, Params) of 56 | RT when RT == <<"token">>; RT == <<"authorization_code">> -> 57 | process_authorization_grant(Req2, RT, Params); 58 | _ -> 59 | cowboy_req:reply(400, [], <<"Bad Request.">>, Req2) 60 | end; 61 | _ -> 62 | cowboy_req:reply(400, [], <<"Bad Request.">>, Req2) 63 | end, 64 | {halt, Reply, State}. 65 | 66 | process_get(Req, State) -> 67 | {QsVals, Req2} = cowboy_req:qs_vals(Req), 68 | ResponseType = proplists:get_value(<<"response_type">>, QsVals), 69 | {ok, Reply} = 70 | case ResponseType of 71 | RT when RT == <<"token">>; RT == <<"authorization_code">> -> 72 | Params = lists:filter( 73 | fun({K, _V}) -> 74 | lists:member(K, [<<"client_id">>, 75 | <<"redirect_uri">>, 76 | <<"scope">>, 77 | <<"state">>]) 78 | end, 79 | QsVals), 80 | show_authorisation_form(Req2, ResponseType, Params); 81 | _ -> 82 | JSON = mochijson2:encode({struct, [{error, <<"unsupported_response_type">>}]}), 83 | cowboy_req:reply(400, [], JSON, Req2) 84 | end, 85 | {halt, Reply, State}. 86 | 87 | %%%=================================================================== 88 | %%% Grant type handlers 89 | %%%=================================================================== 90 | 91 | process_password_grant(Req, Params) -> 92 | Username = proplists:get_value(<<"username">>, Params), 93 | Password = proplists:get_value(<<"password">>, Params), 94 | Scope = get_scope(Params), 95 | AuthResult = oauth2:authorize_password({Username, Password}, Scope, []), 96 | Response = issue_token(AuthResult), 97 | reply(Response, Req). 98 | 99 | process_client_credentials_grant(Req, Params) -> 100 | case cowboy_req:header(<<"authorization">>, Req) of 101 | {<<"Basic ", Credentials/binary>>, Req2} -> 102 | [Id, Secret] = binary:split(base64:decode(Credentials), <<":">>), 103 | Scope = get_scope(Params), 104 | AuthResult = oauth2:authorize_client_credentials({Id, Secret}, Scope, []), 105 | Response = issue_token(AuthResult), 106 | reply(Response, Req2); 107 | _ -> cowboy_req:reply(401, Req) 108 | end. 109 | 110 | show_authorisation_form(Req, ResponseType, Params) -> 111 | State = proplists:get_value(<<"state">>, Params), 112 | Scope = get_scope(Params), 113 | ClientId = proplists:get_value(<<"client_id">>, Params), 114 | RedirectUri = proplists:get_value(<<"redirect_uri">>, Params), 115 | %% Pass the scope, state and redirect URI to the browser 116 | %% as hidden form parameters, allowing them to "propagate" 117 | %% to the next stage. 118 | case ClientId == undefined of 119 | true -> 120 | cowboy_req:reply(400, [], <<"Bad Request.">>, Req); 121 | false -> 122 | {ok, Html} = auth_form_dtl:render([{redirect_uri, RedirectUri}, 123 | {client_id, ClientId}, 124 | {state, State}, 125 | {scope, binary_join(Scope, <<" ">>)}, 126 | {response_type, ResponseType}]), 127 | cowboy_req:reply(200, [], Html, Req) 128 | end. 129 | 130 | % Process response from auth form. 131 | % ResponseType coud be <<"token">> for implicit grant 132 | % or <<"authorization_code">> for authorisation code grant 133 | process_authorization_grant(Req, ResponseType, Params) -> 134 | ClientId = proplists:get_value(<<"client_id">>, Params), 135 | RedirectUri = proplists:get_value(<<"redirect_uri">>, Params), 136 | Username = proplists:get_value(<<"username">>, Params), 137 | Password = proplists:get_value(<<"password">>, Params), 138 | State = proplists:get_value(<<"state">>, Params), 139 | Scope = get_scope(Params), 140 | 141 | ExtraParams = [{<<"state">>, State}], 142 | AuthResult = oauth2:authorize_code_request({Username, Password}, 143 | ClientId, 144 | RedirectUri, Scope, []), 145 | Response = case ResponseType of 146 | <<"token">> -> issue_token(AuthResult); 147 | <<"authorization_code">> -> issue_code(AuthResult) 148 | end, 149 | redirect(RedirectUri, Response, ExtraParams, Req). 150 | 151 | process_authorization_token_grant(Req, Params) -> 152 | ClientId = proplists:get_value(<<"client_id">>, Params), 153 | RedirectUri = proplists:get_value(<<"redirect_uri">>, Params), 154 | Code = proplists:get_value(<<"code">>, Params), 155 | AuthResult = oauth2:authorize_code_grant(ClientId, Code, RedirectUri, []), 156 | Response = issue_token(AuthResult), 157 | reply(Response, Req). 158 | 159 | 160 | %%%=================================================================== 161 | %%% Internal functions 162 | %%%=================================================================== 163 | 164 | refresh_token_grant() -> 165 | application:get_env(rabbitmq_auth_backend_oauth, 166 | grant_refresh_token, 167 | false). 168 | 169 | issue_token({ok, {_, Auth}}) -> 170 | TokenResponse = case refresh_token_grant() of 171 | true -> oauth2:issue_token_and_refresh(Auth, []); 172 | false -> oauth2:issue_token(Auth, []) 173 | end, 174 | case TokenResponse of 175 | {ok, {_, TokenResp}} -> {ok, TokenResp}; 176 | {error, _} = Err -> Err 177 | end; 178 | issue_token({error, Err}) -> 179 | {error, Err}. 180 | 181 | issue_code({ok, {_, Auth}}) -> 182 | case oauth2:issue_code(Auth, []) of 183 | {ok, {_, CodeResp}} -> {ok, CodeResp}; 184 | {error, _} = Err -> Err 185 | end; 186 | issue_code({error, Err}) -> 187 | {error, Err}. 188 | 189 | reply({ok, Response}, Req) -> 190 | Proplist = lists:keydelete(<<"resource_owner">>, 1, 191 | oauth2_response:to_proplist(Response)), 192 | cowboy_req:reply(200, [], mochijson2:encode({struct, Proplist}), Req); 193 | reply({error, Err}, Req) -> 194 | cowboy_req:reply(400, [], 195 | mochijson2:encode({struct, [{<<"error">>, Err}]}), 196 | Req). 197 | 198 | redirect(RedirectUri, {ok, Response}, Extra, Req) -> 199 | Params = oauth2_response:to_proplist(Response) ++ Extra, 200 | redirect(RedirectUri, Params, Req); 201 | redirect(RedirectUri, {error, Err}, Extra, Req) -> 202 | Params = [{<<"error">>, Err} | Extra], 203 | redirect(RedirectUri, Params, Req). 204 | 205 | redirect(RedirectUri, Params, Req) when is_list(Params) -> 206 | BinParams = lists:map( 207 | fun ({K,V}) when is_integer(V) -> {K, integer_to_binary(V)}; 208 | ({K,V}) when is_atom(V) -> {K, atom_to_binary(V, utf8)}; 209 | ({K,V}) when is_binary(V) -> {K,V} 210 | end, 211 | Params), 212 | Frag = cow_qs:qs(BinParams), 213 | Req1 = cowboy_req:set_resp_header(<<"location">>, 214 | <>, 215 | Req), 216 | cowboy_req:reply(302, [], <<>>, Req1). 217 | 218 | 219 | get_scope(Params) -> 220 | binary:split(proplists:get_value(<<"scope">>, Params, <<>>), 221 | <<" ">>, 222 | [global]). 223 | 224 | binary_join([], _) -> <<>>; 225 | binary_join([H], _) -> H; 226 | binary_join([H1, H2 | T], Sep) -> 227 | binary_join([<

> | T], Sep). 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /src/rabbit_oauth2_backend.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_oauth2_backend). 2 | 3 | -behavior(oauth2_backend). 4 | 5 | -include_lib("rabbit_common/include/rabbit.hrl"). 6 | 7 | %%% Behavior API 8 | -export([authenticate_user/2]). 9 | -export([authenticate_client/2]). 10 | -export([get_client_identity/2]). 11 | -export([associate_access_code/3]). 12 | -export([associate_refresh_token/3]). 13 | -export([associate_access_token/3]). 14 | -export([resolve_access_code/2]). 15 | -export([resolve_refresh_token/2]). 16 | -export([resolve_access_token/2]). 17 | -export([revoke_access_code/2]). 18 | -export([revoke_access_token/2]). 19 | -export([revoke_refresh_token/2]). 20 | -export([get_redirection_uri/2]). 21 | -export([verify_redirection_uri/3]). 22 | -export([verify_client_scope/3]). 23 | -export([verify_resowner_scope/3]). 24 | -export([verify_scope/3]). 25 | 26 | -export([add_access_token/3, add_access_token/4]). 27 | -export([oauth2_backend_env/0]). 28 | 29 | -define(BACKEND, rabbit_auth_backend_internal). 30 | 31 | -ifdef(TEST). 32 | -compile(export_all). 33 | -endif. 34 | 35 | oauth2_backend_env() -> 36 | application:set_env(oauth2, backend, rabbit_oauth2_backend). 37 | 38 | add_access_token(Token, Scope, ExpiresIn) when is_list(Scope), 39 | is_integer(ExpiresIn) -> 40 | add_access_token(Token, Scope, ExpiresIn, time_compat:os_system_time(seconds)). 41 | 42 | add_access_token(Token, Scope, ExpiresIn, CreatedAt) 43 | when is_list(Scope), 44 | is_integer(ExpiresIn), 45 | is_integer(CreatedAt) -> 46 | {ok, []} = associate_access_token(Token, 47 | [{<<"scope">>, Scope}, 48 | {<<"expiry_time">>, 49 | ExpiresIn + CreatedAt}], 50 | []), 51 | ok. 52 | 53 | %% Behaviour functions -------------------------------------------------------- 54 | 55 | authenticate_user({Username, Password}, Ctx) -> 56 | rabbit_log:info("User ~p Pass ~p", [Username, Password]), 57 | case ?BACKEND:user_login_authentication(Username, [{password, Password}]) of 58 | {refused, _Err} -> {error, notfound}; 59 | {refused, _Format, _Arg} -> {error, notfound}; 60 | {ok, AuthUser} -> {ok, {Ctx, AuthUser}} 61 | end. 62 | 63 | %% Access token --------------------------------------------------------------- 64 | 65 | associate_access_token(AccessToken, Context, AppContext) -> 66 | ok = rabbit_oauth2_storage:save_access_token(AccessToken, Context), 67 | {ok, AppContext}. 68 | 69 | resolve_access_token(AccessToken, AppContext) -> 70 | case rabbit_oauth2_storage:lookup_access_token(AccessToken) of 71 | {ok, {_, Context}} -> {ok, {AppContext, Context}}; 72 | {error, not_found} -> {error, notfound} 73 | end. 74 | 75 | revoke_access_token(AccessToken, AppContext) -> 76 | ok = rabbit_oauth2_storage:remove_access_token(AccessToken), 77 | {ok, AppContext}. 78 | 79 | %% Access code ---------------------------------------------------------------- 80 | 81 | associate_access_code(AccessCode, Context, AppContext) -> 82 | ok = rabbit_oauth2_storage:save_access_code(AccessCode, Context), 83 | {ok, AppContext}. 84 | 85 | resolve_access_code(AccessCode, AppContext) -> 86 | case rabbit_oauth2_storage:lookup_access_code(AccessCode) of 87 | {ok, {_, Context}} -> {ok, {AppContext, Context}}; 88 | {error, not_found} -> {error, notfound} 89 | end. 90 | 91 | revoke_access_code(AccessCode, AppContext) -> 92 | ok = rabbit_oauth2_storage:remove_access_code(AccessCode), 93 | {ok, AppContext}. 94 | 95 | %% Refresh token -------------------------------------------------------------- 96 | 97 | associate_refresh_token(RefreshToken, Context, AppContext) -> 98 | ok = rabbit_oauth2_storage:save_refresh_token(RefreshToken, Context), 99 | {ok, AppContext}. 100 | 101 | resolve_refresh_token(RefreshToken, AppContext) -> 102 | case rabbit_oauth2_storage:lookup_refresh_token(RefreshToken) of 103 | {ok, {_, Context}} -> {ok, {AppContext, Context}}; 104 | {error, not_found} -> {error, notfound} 105 | end. 106 | 107 | revoke_refresh_token(RefreshToken, AppContext) -> 108 | ok = rabbit_oauth2_storage:remove_refresh_token(RefreshToken), 109 | {ok, AppContext}. 110 | 111 | %% Scope ---------------------------------------------------------------------- 112 | 113 | verify_scope(RScope, Scope, AppContext) when is_list(RScope), is_list(Scope) -> 114 | case Scope -- RScope of 115 | [] -> {ok, {AppContext, Scope}}; 116 | _ -> {error, invalid_scope} 117 | end; 118 | verify_scope(_, _, _) -> {error, invalid_scope}. 119 | 120 | verify_resowner_scope(AuthUser, Scope, Ctx) -> 121 | ScopePermissions = rabbit_oauth2_scope:parse_scope(Scope), 122 | ValidScope = lists:filtermap( 123 | fun({Resource, Permission, ScopeEl}) -> 124 | case ?BACKEND:check_resource_access(AuthUser, 125 | Resource, 126 | Permission) of 127 | false -> false; 128 | true -> {true, ScopeEl} 129 | end 130 | end, 131 | ScopePermissions), 132 | ScopePolicy = application:get_env(rabbitmq_auth_backend_oauth, 133 | scope_policy, 134 | matching), 135 | case {ValidScope, ScopePolicy} of 136 | {[], _} -> {error, invalid_scope}; 137 | {Scope, _} -> {ok, {Ctx, Scope}}; 138 | {_, matching} -> {ok, {Ctx, Scope}}; 139 | _ -> {error, invalid_scope} 140 | end. 141 | 142 | verify_client_scope({_, _, _, CScope}, Scope, Ctx) when is_list(CScope), 143 | is_list(Scope) -> 144 | case Scope -- CScope of 145 | [] -> {ok, {Ctx, Scope}}; 146 | _ -> {error, invalid_scope} 147 | end; 148 | verify_client_scope(_, _, _) -> {error, invalid_scope}. 149 | 150 | %% Client --------------------------------------------------------------------- 151 | 152 | authenticate_client({ClientId, Secret}, Ctx) -> 153 | case rabbit_oauth2_storage:lookup_client(ClientId) of 154 | {ok, Client = {ClientId, Secret, _, _}} -> {ok, {Ctx, Client}}; 155 | {ok, _} -> {error, badsecret}; 156 | {error, not_found} -> {error, notfound} 157 | end; 158 | authenticate_client(ClientId, Ctx) -> 159 | case rabbit_oauth2_storage:lookup_client(ClientId) of 160 | {ok, Client} -> {ok, {Ctx, Client}}; 161 | {error, not_found} -> {error, notfound} 162 | end. 163 | 164 | get_client_identity(ClientId, Ctx) -> 165 | case rabbit_oauth2_storage:lookup_client(ClientId) of 166 | {ok, Client} -> {ok, {Ctx, Client}}; 167 | {error, not_found} -> {error, notfound} 168 | end. 169 | 170 | get_redirection_uri({ClientId, Secret}, Ctx) -> 171 | case rabbit_oauth2_storage:lookup_client(ClientId) of 172 | {ok, {ClientId, Secret, RedirUrl, _}} -> {ok, {Ctx, RedirUrl}}; 173 | _ -> {error, notfound} 174 | end. 175 | 176 | 177 | verify_redirection_uri({_, _, RedirUrl, _}, RedirUrl, Ctx) -> {ok, Ctx}; 178 | verify_redirection_uri(_, _, _) -> {error, mismatch}. 179 | 180 | 181 | -------------------------------------------------------------------------------- /src/rabbit_oauth2_scope.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_oauth2_scope). 2 | 3 | -export([vhost_access/2, resource_access/3]). 4 | -export([parse_scope/1]). 5 | 6 | -include_lib("rabbit_common/include/rabbit.hrl"). 7 | 8 | %% API functions -------------------------------------------------------------- 9 | 10 | vhost_access(VHost, Ctx) -> 11 | lists:any( 12 | fun({#resource{ virtual_host = VH }, _}) -> 13 | VH == VHost 14 | end, 15 | get_scope_permissions(Ctx)). 16 | 17 | resource_access(Resource, Permission, Ctx) -> 18 | lists:any( 19 | fun({Res, Perm}) -> 20 | Res == Resource andalso Perm == Permission 21 | end, 22 | get_scope_permissions(Ctx)). 23 | 24 | %% Internal ------------------------------------------------------------------- 25 | 26 | get_scope_permissions(Ctx) -> 27 | case lists:keyfind(<<"scope">>, 1, Ctx) of 28 | {_, Scope} -> 29 | [ {Res, Perm} || {Res, Perm, _ScopeEl} <- parse_scope(Scope) ]; 30 | false -> [] 31 | end. 32 | 33 | parse_scope(Scope) when is_list(Scope) -> 34 | lists:filtermap( 35 | fun(ScopeEl) -> 36 | case parse_scope_el(ScopeEl) of 37 | ignore -> false; 38 | Perm -> {true, Perm} 39 | end 40 | end, 41 | Scope). 42 | 43 | parse_scope_el(ScopeEl) when is_binary(ScopeEl) -> 44 | case binary:split(ScopeEl, <<"_">>, [global]) of 45 | [VHost, KindCode, PermCode | Name] -> 46 | Kind = case KindCode of 47 | <<"q">> -> queue; 48 | <<"ex">> -> exchange; 49 | <<"t">> -> topic; 50 | _ -> ignore 51 | end, 52 | Permission = case PermCode of 53 | <<"configure">> -> configure; 54 | <<"write">> -> write; 55 | <<"read">> -> read; 56 | _ -> ignore 57 | end, 58 | case Kind == ignore orelse Permission == ignore orelse Name == [] of 59 | true -> ignore; 60 | false -> 61 | { 62 | #resource{ 63 | virtual_host = VHost, 64 | kind = Kind, 65 | name = binary_join(Name, <<"_">>)}, 66 | Permission, 67 | ScopeEl 68 | } 69 | end; 70 | _ -> ignore 71 | end. 72 | 73 | binary_join([B|Bs], Sep) -> 74 | iolist_to_binary([B|add_separator(Bs, Sep)]); 75 | binary_join([], _Sep) -> 76 | <<>>. 77 | 78 | add_separator([B|Bs], Sep) -> 79 | [Sep, B | add_separator(Bs, Sep)]; 80 | add_separator([], _) -> 81 | []. 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/rabbit_oauth2_storage.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_oauth2_storage). 2 | 3 | -export([ 4 | save_client/4, 5 | save_access_code/2, 6 | save_access_token/2, 7 | save_refresh_token/2, 8 | 9 | lookup_client/1, 10 | lookup_access_code/1, 11 | lookup_access_token/1, 12 | lookup_refresh_token/1, 13 | 14 | remove_client/1, 15 | remove_access_code/1, 16 | remove_access_token/1, 17 | remove_refresh_token/1 18 | ]). 19 | 20 | -export([setup_schema/0]). 21 | 22 | -record(token, { 23 | token, 24 | context 25 | }). 26 | 27 | -record(client,{ 28 | client_id, 29 | client_secret, 30 | redirect_uri, 31 | scope 32 | }). 33 | 34 | -define(ACCESS_TOKEN_TABLE, rabbit_oauth2_access_token). 35 | -define(ACCESS_CODE_TABLE, rabbit_oauth2_access_code). 36 | -define(REFRESH_TOKEN_TABLE, rabbit_oauth2_refresh_token). 37 | -define(CLIENT_TABLE, rabbit_oauth2_client). 38 | 39 | setup_schema() -> 40 | mnesia:create_table(?ACCESS_TOKEN_TABLE, 41 | [{attributes, record_info(fields, token)}, 42 | {record_name, token}, 43 | {type, set}]), 44 | mnesia:create_table(?REFRESH_TOKEN_TABLE, 45 | [{attributes, record_info(fields, token)}, 46 | {record_name, token}, 47 | {type, set}]), 48 | mnesia:create_table(?CLIENT_TABLE, 49 | [{attributes, record_info(fields, client)}, 50 | {record_name, client}, 51 | {type, set}]), 52 | mnesia:create_table(?ACCESS_CODE_TABLE, 53 | [{attributes, record_info(fields, token)}, 54 | {record_name, token}, 55 | {type, set}]), 56 | mnesia:add_table_copy(?ACCESS_TOKEN_TABLE, node(), ram_copies), 57 | mnesia:add_table_copy(?REFRESH_TOKEN_TABLE, node(), ram_copies), 58 | mnesia:add_table_copy(?CLIENT_TABLE, node(), ram_copies), 59 | mnesia:add_table_copy(?ACCESS_CODE_TABLE, node(), ram_copies), 60 | 61 | mnesia:wait_for_tables([?ACCESS_TOKEN_TABLE, 62 | ?REFRESH_TOKEN_TABLE, 63 | ?CLIENT_TABLE, 64 | ?ACCESS_CODE_TABLE], 30000). 65 | 66 | save_client(ClientId, Secret, RedirUri, Scope) -> 67 | Client = #client{ 68 | client_id = ClientId, 69 | client_secret = Secret, 70 | redirect_uri = RedirUri, 71 | scope = Scope}, 72 | save(?CLIENT_TABLE, Client). 73 | 74 | save_access_token(Token, Context) -> 75 | save_token(?ACCESS_TOKEN_TABLE, Token, Context). 76 | 77 | save_refresh_token(Token, Context) -> 78 | save_token(?REFRESH_TOKEN_TABLE, Token, Context). 79 | 80 | save_access_code(Code, Context) -> 81 | save_token(?ACCESS_CODE_TABLE, Code, Context). 82 | 83 | save_token(Table, Token, Context) -> 84 | TokenRecord = #token{ token = Token, context = Context }, 85 | save(Table, TokenRecord). 86 | 87 | save(Table, Data) -> 88 | rabbit_misc:execute_mnesia_transaction( 89 | fun () -> 90 | ok = mnesia:write(Table, Data, write) 91 | end). 92 | 93 | 94 | lookup_client(ClientId) -> 95 | case lookup(?CLIENT_TABLE, ClientId) of 96 | {error, not_found} -> {error, not_found}; 97 | {ok, #client{client_secret = Secret, 98 | redirect_uri = RedirUri, 99 | scope = Scope}} -> 100 | {ok, {ClientId, Secret, RedirUri, Scope}} 101 | end. 102 | 103 | lookup_access_token(Token) -> 104 | lookup_token(?ACCESS_TOKEN_TABLE, Token). 105 | 106 | lookup_refresh_token(Token) -> 107 | lookup_token(?REFRESH_TOKEN_TABLE, Token). 108 | 109 | lookup_access_code(Code) -> 110 | lookup_token(?ACCESS_CODE_TABLE, Code). 111 | 112 | lookup_token(Table, Token) -> 113 | case lookup(Table, Token) of 114 | {error, not_found} -> {error, not_found}; 115 | {ok, #token{context = Context}} -> 116 | {ok, {Token, Context}} 117 | end. 118 | 119 | lookup(Table, Key) -> 120 | rabbit_misc:dirty_read({Table, Key}). 121 | 122 | remove_client(ClientId) -> 123 | delete(?CLIENT_TABLE, ClientId). 124 | 125 | remove_access_token(Token) -> 126 | delete(?ACCESS_TOKEN_TABLE, Token). 127 | 128 | remove_refresh_token(Token) -> 129 | delete(?REFRESH_TOKEN_TABLE, Token). 130 | 131 | remove_access_code(Code) -> 132 | delete(?ACCESS_CODE_TABLE, Code). 133 | 134 | delete(Table, Key) -> 135 | rabbit_misc:execute_mnesia_transaction( 136 | fun() -> 137 | ok = mnesia:delete({Table, Key}) 138 | end). 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/rabbitmq_auth_backend_oauth.app.src: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | {application, rabbitmq_auth_backend_oauth, 3 | [{description, "RabbitMQ OAuth2 Authentication Backend"}, 4 | {vsn, ""}, 5 | {modules, [rabbit_auth_backend_oauth_app, rabbit_auth_backend_oauth, rabbit_oauth2_backend]}, 6 | {registered, []}, 7 | {mod, {rabbit_auth_backend_oauth_app, []}}, 8 | {env, [ 9 | {auth_server, 10 | {internal, [{port, 15672}]}}]}, 11 | {applications, [kernel, stdlib, inets, cowlib, cowboy, rabbitmq_web_dispatch]}]}. 12 | -------------------------------------------------------------------------------- /templates/auth_form.dtl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 |
    24 |
  • 25 |
  • 26 |
  • 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /test/src/mock_transport.erl: -------------------------------------------------------------------------------- 1 | -module(mock_transport). 2 | 3 | -export([name/0, send/2]). 4 | 5 | name() -> mock. 6 | 7 | send(_,_) -> ok. -------------------------------------------------------------------------------- /test/src/rabbit_oauth2_auth_test.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_oauth2_auth_test). 2 | 3 | -compile(export_all). 4 | 5 | tests() -> 6 | test_get(), 7 | test_grant_type(), 8 | test_resp_type(), 9 | % test_refresh(), 10 | passed. 11 | 12 | 13 | test_get() -> 14 | % TODO: error cases. 15 | % There will be same result (auth form) for each response type 16 | RespTypes = [<<"token">>, <<"authorization_code">>], 17 | ClientId = <<"foo">>, 18 | RedirectUri = <<"https://example.com">>, 19 | Scope = <<"sope_scope other_scope">>, 20 | State = <<"random_state">>, 21 | Params = [{<<"client_id">>, ClientId}, 22 | {<<"redirect_uri">>, RedirectUri}, 23 | {<<"scope">>, Scope}, 24 | {<<"state">>, State}], 25 | lists:foreach(fun(RespType) -> 26 | {ok, {200, RespBody}} = http_get(Params ++ 27 | [{<<"response_type">>, RespType}]), 28 | <<"", _/binary>> = RespBody, 29 | {_,_} = binary:match(RespBody, 30 | <<"">>), 33 | {_,_} = binary:match(RespBody, 34 | <<"">>), 37 | {_,_} = binary:match(RespBody, 38 | <<"">>), 41 | {_,_} = binary:match(RespBody, 42 | <<"">>), 45 | {_,_} = binary:match(RespBody, 46 | <<"">>) 49 | end, 50 | RespTypes). 51 | 52 | test_grant_type() -> 53 | Scope = <<"/_q_configure_foo /_ex_configure_bar">>, 54 | Scopes = binary:split(Scope, <<" ">>, [global]), 55 | ClientId = <<"foo">>, 56 | ClientSecret = <<"bar">>, 57 | RedirectUri = <<"https://example.com">>, 58 | Username = <<"guest">>, 59 | Password = <<"guest">>, 60 | ClientAuth = base64:encode_to_string(<>), 62 | ok = create_client(ClientId, ClientSecret, RedirectUri, Scopes), 63 | {ok, AuthCode} = create_code(ClientId, ClientSecret, Scopes), 64 | GrantTypes = [ 65 | {<<"password">>, 66 | [{<<"username">>, Username}, 67 | {<<"password">>, Password}, 68 | {<<"scope">>, Scope}], 69 | []} 70 | ,{<<"client_credentials">>, 71 | [{<<"scope">>, Scope}], 72 | [{"Authorization", "Basic " ++ ClientAuth}]} 73 | ,{<<"authorization_code">>, 74 | [{<<"client_id">>, ClientId}, 75 | {<<"redirect_uri">>, RedirectUri}, 76 | {<<"code">>, AuthCode}], 77 | []} 78 | ], 79 | lists:foreach( 80 | fun({CodeGrant, Params, Headers}) -> 81 | {ok, {200, Result, _}} = http_post([{<<"grant_type">>, CodeGrant} 82 | | Params], Headers), 83 | {struct, ResultData} = mochijson2:decode(Result), 84 | {<<"access_token">>, AccessToken} = proplists:lookup(<<"access_token">>, ResultData), 85 | {<<"expires_in">>, Expiry} = proplists:lookup(<<"expires_in">>, ResultData), 86 | {<<"scope">>, Scope} = proplists:lookup(<<"scope">>, ResultData), 87 | {<<"token_type">>, <<"bearer">>} = proplists:lookup(<<"token_type">>, ResultData), 88 | ok = access_token(AccessToken, Scopes) 89 | end, 90 | GrantTypes). 91 | 92 | test_resp_type() -> 93 | Scope = <<"/_q_configure_foo /_ex_configure_bar">>, 94 | Scopes = binary:split(Scope, <<" ">>, [global]), 95 | ClientId = <<"foo">>, 96 | ClientSecret = <<"bar">>, 97 | RedirectUri = <<"https://example.com">>, 98 | Username = <<"guest">>, 99 | Password = <<"guest">>, 100 | State = <<"Some state">>, 101 | 102 | RespTypes = [<<"token">>, <<"authorization_code">>], 103 | Params = [{<<"client_id">>, ClientId}, 104 | {<<"redirect_uri">>, RedirectUri}, 105 | {<<"username">>, Username}, 106 | {<<"password">>, Password}, 107 | {<<"state">>, State}, 108 | {<<"scope">>, Scope}], 109 | lists:foreach( 110 | fun(RespType) -> 111 | {ok, {302, _, Headers}} = http_post([{<<"response_type">>, RespType} 112 | | Params], []), 113 | {"location", Location} = proplists:lookup("location", Headers), 114 | [Url, Qs] = string:tokens(Location, "#"), 115 | RedirectUri = list_to_binary(Url), 116 | ResultData = cow_qs:parse_qs(list_to_binary(Qs)), 117 | case RespType of 118 | <<"token">> -> 119 | {<<"access_token">>, AccessToken} = proplists:lookup(<<"access_token">>, ResultData), 120 | {<<"expires_in">>, Expiry} = proplists:lookup(<<"expires_in">>, ResultData), 121 | {<<"scope">>, Scope} = proplists:lookup(<<"scope">>, ResultData), 122 | {<<"token_type">>, <<"bearer">>} = proplists:lookup(<<"token_type">>, ResultData), 123 | ok = access_token(AccessToken, Scopes); 124 | <<"authorization_code">> -> 125 | {<<"access_code">>, AccessCode} = proplists:lookup(<<"access_code">>, ResultData), 126 | {<<"expires_in">>, Expiry} = proplists:lookup(<<"expires_in">>, ResultData), 127 | {<<"scope">>, Scope} = proplists:lookup(<<"scope">>, ResultData), 128 | {<<"token_type">>, <<"bearer">>} = proplists:lookup(<<"token_type">>, ResultData), 129 | ok = access_code(AccessCode, ClientId, ClientSecret, RedirectUri) 130 | end 131 | end, 132 | RespTypes). 133 | 134 | access_token(AccessToken, Scope) -> 135 | {ok, {n, Ctx}} = oauth2:verify_access_token(AccessToken, n), 136 | Scope = proplists:get_value(<<"scope">>, Ctx), 137 | ok. 138 | 139 | access_code(AccessCode, ClientId, ClientSecret, RedirectUri) -> 140 | {ok, {n, _}} = oauth2:authorize_code_grant({ClientId, ClientSecret}, 141 | AccessCode, RedirectUri, n), 142 | ok. 143 | 144 | create_client(ClientId, ClientSecret, RedirectUri, Scope) -> 145 | rabbit_oauth2_storage:save_client(ClientId, ClientSecret, RedirectUri, Scope). 146 | 147 | create_code(ClientId, Secret, Scope) -> 148 | {ok, {n, Auth}} = oauth2:authorize_client_credentials({ClientId, Secret}, Scope, n), 149 | {ok, {n, CodeResp}} = oauth2:issue_code(Auth, n), 150 | {ok, Code} = oauth2_response:access_code(CodeResp). 151 | 152 | http_get(Params) -> 153 | Qs = cow_qs:qs(Params), 154 | Url = <<"http://localhost:15672/oauth?", Qs/binary>>, 155 | {ok, {{_, Status, _}, _, Body}} = httpc:request(binary_to_list(Url)), 156 | {ok, {Status, list_to_binary(Body)}}. 157 | 158 | http_post(Params, Headers) -> 159 | Qs = cow_qs:qs(Params), 160 | Url = "http://localhost:15672/oauth", 161 | {ok, {{_, Status, _}, RespHeaders, Body}} = httpc:request(post, {Url, Headers, "application/x-www-form-urlencoded", Qs}, [], []), 162 | {ok, {Status, list_to_binary(Body), RespHeaders}}. 163 | -------------------------------------------------------------------------------- /test/src/rabbit_oauth2_backend_test.erl: -------------------------------------------------------------------------------- 1 | -module(rabbit_oauth2_backend_test). 2 | 3 | -compile(export_all). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | -include_lib("rabbit_common/include/rabbit.hrl"). 6 | 7 | standalone_tests() -> 8 | parse_scope_test(), 9 | scope_permissions_test(), 10 | passed. 11 | 12 | broker_tests() -> 13 | save_load_token_test(), 14 | revoke_token_test(), 15 | expire_token_test(), 16 | token_permission_test(), 17 | client_auth_grant_test(), 18 | access_code_grant_test(), 19 | passed. 20 | 21 | parse_scope_test() -> 22 | Scopes = [ 23 | % VHost_Kind_Permission_Name 24 | {<<"vhost_q_configure_foo">>, {<<"vhost">>, queue, <<"foo">>, configure}}, 25 | {<<"vhost_q_write_foo">>, {<<"vhost">>, queue, <<"foo">>, write}}, 26 | {<<"vhost_q_read_foo">>, {<<"vhost">>, queue, <<"foo">>, read}}, 27 | {<<"vhost_ex_configure_foo">>, {<<"vhost">>, exchange, <<"foo">>, configure}}, 28 | {<<"vhost_ex_write_foo">>, {<<"vhost">>, exchange, <<"foo">>, write}}, 29 | {<<"vhost_ex_read_foo">>, {<<"vhost">>, exchange, <<"foo">>, read}}, 30 | {<<"vhost_t_write_foo">>, {<<"vhost">>, topic, <<"foo">>, write}}, 31 | % Name can contain '_' 32 | {<<"vhost_q_configure_foo_bar_baz">>, {<<"vhost">>, queue, <<"foo_bar_baz">>, configure}}, 33 | % Vhost cannot contain '_' 34 | {<<"vhost_foo_q_configure_foo_bar">>, ignore}, 35 | % Vhost and name can contain different characters 36 | {<<"vhost.com/foo_q_configure_foo.bar,baz">>, {<<"vhost.com/foo">>, queue, <<"foo.bar,baz">>, configure}}, 37 | % Kind and Permission should be valid 38 | {<<"vhost_qu_configure_name">>, ignore}, 39 | {<<"vhost_q_noconfigure_name">>, ignore}, 40 | % There should be all parts 41 | {<<"vhost_q_conf">>, ignore}, 42 | {<<"vhost_q_name">>, ignore}, 43 | {<<"q_configure_name">>, ignore}, 44 | % '/' for default host 45 | {<<"/_q_configure_foo">>, {<<"/">>, queue, <<"foo">>, configure}}, 46 | % Utf? 47 | {<<"/_q_configure_ПиуПиу"/utf8>>, {<<"/">>, queue, <<"ПиуПиу"/utf8>>, configure}} 48 | ], 49 | lists:foreach( 50 | fun({Scope, Result}) -> 51 | case rabbit_oauth2_backend:parse_scope_el(Scope) of 52 | ignore -> 53 | Result = ignore; 54 | {#resource{ virtual_host = VHost, kind = Kind, name = Name }, 55 | Permission, Scope} -> 56 | Result = {VHost, Kind, Name, Permission} 57 | end 58 | end, 59 | Scopes). 60 | save_load_token_test() -> 61 | {error, not_found} = rabbit_oauth2_storage:lookup_access_token(<<"token">>), 62 | {ok, []} = rabbit_oauth2_backend:associate_access_token(<<"token1">>, [{<<"scope">>, [<<"FOO">>]}], []), 63 | {ok, {<<"token1">>, [{<<"scope">>, [<<"FOO">>]}]}} = rabbit_oauth2_storage:lookup_access_token(<<"token1">>), 64 | {error, not_found} = rabbit_oauth2_storage:lookup_access_token(<<"token">>), 65 | TimeSec = time_compat:os_system_time(seconds), 66 | ok = rabbit_oauth2_backend:add_access_token(<<"token2">>, [<<"foo">>, <<"bar">>], 100, TimeSec), 67 | Context = [{<<"scope">>, [<<"foo">>, <<"bar">>]}, {<<"expiry_time">>, 100 + TimeSec}], 68 | {ok, {[], Context}} = rabbit_oauth2_backend:resolve_access_token(<<"token2">>, []). 69 | 70 | revoke_token_test() -> 71 | TimeSec = time_compat:os_system_time(seconds), 72 | ok = rabbit_oauth2_backend:add_access_token(<<"token3">>, [<<"foo">>, <<"bar">>], 100, TimeSec), 73 | {ok, foo} = rabbit_oauth2_backend:revoke_access_token(<<"token3">>, foo), 74 | {error, access_denied} = oauth2:verify_access_token(<<"token3">>, []). 75 | 76 | expire_token_test() -> 77 | TimeSec = time_compat:os_system_time(seconds), 78 | ok = rabbit_oauth2_backend:add_access_token(<<"token3">>, [<<"foo">>, <<"bar">>], 1, TimeSec), 79 | timer:sleep(1500), 80 | {error, access_denied} = oauth2:verify_access_token(<<"token3">>, []). 81 | 82 | scope_permissions_test() -> 83 | Examples = [ 84 | % VHost_Kind_Permission_Name 85 | {<<"vhost_q_configure_foo">>, {<<"vhost">>, queue, <<"foo">>, configure}}, 86 | {<<"vhost_q_write_foo">>, {<<"vhost">>, queue, <<"foo">>, write}}, 87 | {<<"vhost_q_read_foo">>, {<<"vhost">>, queue, <<"foo">>, read}}, 88 | {<<"vhost_ex_configure_foo">>, {<<"vhost">>, exchange, <<"foo">>, configure}}, 89 | {<<"vhost_ex_write_foo">>, {<<"vhost">>, exchange, <<"foo">>, write}}, 90 | {<<"vhost_ex_read_foo">>, {<<"vhost">>, exchange, <<"foo">>, read}}, 91 | {<<"vhost_t_write_foo">>, {<<"vhost">>, topic, <<"foo">>, write}}, 92 | {<<"vhost_q_configure_foo_bar_baz">>, {<<"vhost">>, queue, <<"foo_bar_baz">>, configure}}, 93 | {<<"vhost.com/foo_q_configure_foo.bar,baz">>, {<<"vhost.com/foo">>, queue, <<"foo.bar,baz">>, configure}}, 94 | {<<"/_q_configure_foo">>, {<<"/">>, queue, <<"foo">>, configure}}, 95 | {<<"/_q_configure_ПиуПиу"/utf8>>, {<<"/">>, queue, <<"ПиуПиу"/utf8>>, configure}} 96 | ], 97 | lists:foreach( 98 | fun(Example) -> 99 | {Scope, {Vhost, Kind, Name, Permission}} = Example, 100 | Resource = #resource{ virtual_host = Vhost, kind = Kind, name = Name}, 101 | Context = [{<<"scope">>, [Scope]}], 102 | true = rabbit_oauth2_backend:vhost_access(Vhost, Context), 103 | true = rabbit_oauth2_backend:resource_access(Resource, Permission, Context) 104 | end, 105 | Examples). 106 | 107 | 108 | token_permission_test() -> 109 | TimeSec = time_compat:os_system_time(seconds), 110 | ok = rabbit_oauth2_backend:add_access_token(<<"token4">>, [<<"/_q_configure_foo">>], 1000, TimeSec), 111 | {refused, _, _} = rabbit_auth_backend_oauth:user_login_authentication(<<"token3">>, []), 112 | {ok, #auth_user{ username = <<"token4">> } = AuthUser} = 113 | rabbit_auth_backend_oauth:user_login_authentication(<<"token4">>, []), 114 | {ok, none, []} = rabbit_auth_backend_oauth:user_login_authorization(<<"token4">>), 115 | true = rabbit_auth_backend_oauth:check_vhost_access(AuthUser, <<"/">>, none), 116 | false = rabbit_auth_backend_oauth:check_vhost_access(AuthUser, <<"other">>, none), 117 | true = rabbit_auth_backend_oauth:check_resource_access( 118 | AuthUser, 119 | #resource{ virtual_host = <<"/">>, kind = queue, name = <<"foo">>}, 120 | configure), 121 | false = rabbit_auth_backend_oauth:check_resource_access( 122 | AuthUser, 123 | #resource{ virtual_host = <<"other">>, kind = queue, name = <<"foo">>}, 124 | configure), 125 | false = rabbit_auth_backend_oauth:check_resource_access( 126 | AuthUser, 127 | #resource{ virtual_host = <<"/">>, kind = queue, name = <<"foo1">>}, 128 | configure), 129 | false = rabbit_auth_backend_oauth:check_resource_access( 130 | AuthUser, 131 | #resource{ virtual_host = <<"/">>, kind = exchange, name = <<"foo">>}, 132 | configure), 133 | false = rabbit_auth_backend_oauth:check_resource_access( 134 | AuthUser, 135 | #resource{ virtual_host = <<"/">>, kind = queue, name = <<"foo">>}, 136 | write). 137 | 138 | 139 | client_auth_grant_test() -> 140 | ClientId = <<"foo">>, 141 | Secret = <<"bar">>, 142 | RedirUrl = <<"localhost">>, 143 | Scope = [<<"/_q_configure_foo">>], 144 | ok = rabbit_oauth2_storage:save_client(ClientId, Secret, 145 | RedirUrl, Scope), 146 | {ok, {n, Auth}} = oauth2:authorize_client_credentials({ClientId, Secret}, 147 | Scope, n), 148 | {ok, {n, CodeResp}} = oauth2:issue_code(Auth, n), 149 | {ok, Code} = oauth2_response:access_code(CodeResp), 150 | {ok, {n, Auth1}} = oauth2:authorize_code_grant({ClientId, Secret}, 151 | Code, RedirUrl, n), 152 | {ok, {n, TokenResp}} = oauth2:issue_token(Auth1, n), 153 | {error, invalid_authorization} = oauth2:issue_token_and_refresh(Auth1, n), 154 | {ok, AuthToken} = oauth2_response:access_token(TokenResp), 155 | {ok, {n, Ctx}} = oauth2:verify_access_token(AuthToken, n), 156 | Scope = proplists:get_value(<<"scope">>, Ctx). 157 | 158 | access_code_grant_test() -> 159 | ClientId = <<"foo1">>, 160 | Secret = <<"bar1">>, 161 | RedirUrl = <<"localhost">>, 162 | Scope = [<<"/_q_configure_foo">>], 163 | Username = <<"Derp">>, 164 | Password = <<"Pass">>, 165 | ok = rabbit_auth_backend_internal:add_user(Username, Password), 166 | ok = rabbit_auth_backend_internal:set_permissions(Username, <<"/">>, 167 | <<"fo.*">>, 168 | <<"fo.*">>, 169 | <<"fo.*">>), 170 | ok = rabbit_oauth2_storage:save_client(ClientId, Secret, RedirUrl, Scope), 171 | {ok, {n, Auth}} = oauth2:authorize_password({Username, Password}, 172 | {ClientId, Secret}, 173 | RedirUrl, Scope, n), 174 | {ok, {n, CodeResp}} = oauth2:issue_code(Auth, n), 175 | {ok, Code} = oauth2_response:access_code(CodeResp), 176 | 177 | {ok, {n, Auth1}} = oauth2:authorize_code_grant({ClientId, Secret}, 178 | Code, RedirUrl, n), 179 | {ok, {n, TokenResp}} = oauth2:issue_token(Auth1, n), 180 | {ok, {n, RefreshTokenResp}} = oauth2:issue_token_and_refresh(Auth1, n), 181 | {ok, RefreshToken} = oauth2_response:refresh_token(RefreshTokenResp), 182 | {ok, {n, TokenResp1}} = oauth2:refresh_access_token({ClientId, Secret}, 183 | RefreshToken, Scope, n). 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | --------------------------------------------------------------------------------