├── .gitignore ├── .travis.yml ├── API_ACCOUNT.md ├── API_AUTHENTICATION.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── ct-run-docker.sh ├── erlang.mk ├── priv ├── keys │ ├── example.priv.pem │ ├── example.pub.pem │ ├── idp.priv.pem │ └── idp.pub.pem ├── riak-kv │ └── schemas │ │ ├── idp_account.xml │ │ └── idp_account_aclsubject.xml └── ssl │ ├── idp-ca.crt │ ├── idp.crt │ ├── idp.key │ └── idp.pem ├── rel ├── sys.config └── vm.args ├── relx.config ├── run-docker.sh ├── src ├── idp.erl ├── idp_account.erl ├── idp_account_auth.erl ├── idp_accounth.erl ├── idp_accounth_stub.erl ├── idp_app.erl ├── idp_auth.erl ├── idp_cli_account.erl ├── idp_constraint.erl ├── idp_http.erl ├── idp_http_log.erl ├── idp_httph_account.erl ├── idp_httph_account_auth.erl ├── idp_httph_account_auths.erl ├── idp_httph_account_enabled.erl ├── idp_httph_account_refresh.erl ├── idp_httph_account_revoke.erl ├── idp_httph_oauth2_token.erl ├── idp_httpm_cors.erl ├── idp_log.hrl ├── idp_streamh_log.erl └── idp_sup.erl └── test ├── account_SUITE.erl ├── account_auth_SUITE.erl ├── account_enabled_SUITE.erl ├── auth_SUITE.erl └── idp_cth.erl /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | 7 | # Vim 8 | .*.sw[a-z] 9 | *.un~ 10 | Session.vim 11 | 12 | # Erlang 13 | deps 14 | logs 15 | ebin 16 | doc 17 | log 18 | _rel 19 | relx 20 | erl_crash.dump 21 | .erlang.mk 22 | *.beam 23 | *.plt 24 | *.d 25 | 26 | ## Project 27 | .develop-environment 28 | .docker.event* 29 | priv/riak/modules/beam 30 | priv/riak/schemas/acl.xml 31 | *.tar.* 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 19.3 4 | - 20.0 5 | services: 6 | - docker 7 | install: make deps plt 8 | script: make dialyze eunit EUNIT_OPTS=verbose && ./ct-run-docker.sh 9 | notifications: 10 | email: 11 | on_success: never 12 | on_failure: always 13 | 14 | -------------------------------------------------------------------------------- /API_ACCOUNT.md: -------------------------------------------------------------------------------- 1 | # Account 2 | 3 | ## Read 4 | 5 | Returns the account. 6 | 7 | **URI** 8 | 9 | ``` 10 | GET /accounts/${KEY} 11 | ``` 12 | 13 | **URI parameters** 14 | 15 | Name | Type | Default | Description 16 | --------- | ------ | ---------- | ------------------ 17 | KEY | string | _required_ | Account identifier or `me` 18 | 19 | **Response** 20 | 21 | Account 22 | 23 | **Example** 24 | 25 | ```bash 26 | curl -fsSL \ 27 | -XPOST ${ENDPOINT}/accounts/me \ 28 | -H"Authorization: Bearer ${ACCESS_TOKEN}" \ 29 | | jq '.' 30 | 31 | { 32 | "id": "9074b6aa-a980-44e9-8973-29501900aa79" 33 | } 34 | ``` 35 | 36 | 37 | 38 | ## Delete 39 | 40 | Removes the account. 41 | 42 | *NOTE: the operation is only allowed for admins (members of 'admin' predefined group)* 43 | 44 | **URI** 45 | 46 | ``` 47 | DELETE /accounts/${KEY} 48 | ``` 49 | 50 | **URI parameters** 51 | 52 | Name | Type | Default | Description 53 | --------- | ------ | ---------- | ------------------ 54 | KEY | string | _required_ | Account identifier or `me` 55 | 56 | **Response** 57 | 58 | Removed account 59 | 60 | **Example** 61 | 62 | ```bash 63 | curl -fsSL \ 64 | -XDELETE ${ENDPOINT}/accounts/me \ 65 | -H"Authorization: Bearer ${ACCESS_TOKEN}" \ 66 | | jq '.' 67 | 68 | { 69 | "id": "9074b6aa-a980-44e9-8973-29501900aa79" 70 | } 71 | ``` 72 | 73 | 74 | 75 | ## Check if enabled 76 | 77 | Returns **204 Success** status code if account is enabled, otherwise - **404 Not Found**. 78 | 79 | *NOTE: the operation is only allowed for admins (members of 'admin' predefined group)* 80 | 81 | **URI** 82 | 83 | ``` 84 | GET /accounts/${KEY}/enabled 85 | ``` 86 | 87 | **URI parameters** 88 | 89 | Name | Type | Default | Description 90 | --------- | ------ | ---------- | ------------------ 91 | KEY | string | _required_ | Account identifier or `me` 92 | 93 | **Example** 94 | 95 | ```bash 96 | curl -fsSL \ 97 | -XGET ${ENDPOINT}/accounts/9074b6aa-a980-44e9-8973-29501900aa79/disabled \ 98 | -H"Authorization: Bearer ${ACCESS_TOKEN}" \ 99 | | jq '.' 100 | ``` 101 | 102 | 103 | 104 | ## Enable 105 | 106 | Enables account. 107 | 108 | *NOTE: the operation is only allowed for admins (members of 'admin' predefined group)* 109 | 110 | **URI** 111 | 112 | ``` 113 | PUT /accounts/${KEY}/enabled 114 | ``` 115 | 116 | **URI parameters** 117 | 118 | Name | Type | Default | Description 119 | --------- | ------ | ---------- | ------------------ 120 | KEY | string | _required_ | Account identifier or `me` 121 | 122 | **Example** 123 | 124 | ```bash 125 | curl -fsSL \ 126 | -XPUT ${ENDPOINT}/accounts/9074b6aa-a980-44e9-8973-29501900aa79/disabled \ 127 | -H"Authorization: Bearer ${ACCESS_TOKEN}" \ 128 | | jq '.' 129 | ``` 130 | 131 | 132 | 133 | ## Dissable 134 | 135 | Disables account. 136 | 137 | *NOTE: the operation is only allowed for admins (members of 'admin' predefined group)* 138 | 139 | **URI** 140 | 141 | ``` 142 | DELETE /accounts/${KEY}/enabled 143 | ``` 144 | 145 | **URI parameters** 146 | 147 | Name | Type | Default | Description 148 | --------- | ------ | ---------- | ------------------ 149 | KEY | string | _required_ | Account identifier or `me` 150 | 151 | **Example** 152 | 153 | ```bash 154 | curl -fsSL \ 155 | -XDELETE ${ENDPOINT}/accounts/9074b6aa-a980-44e9-8973-29501900aa79/disabled \ 156 | -H"Authorization: Bearer ${ACCESS_TOKEN}" \ 157 | | jq '.' 158 | ``` -------------------------------------------------------------------------------- /API_AUTHENTICATION.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | ## Retrieve access token 4 | 5 | Issues access and refresh tokens of account. For **OAuth2 Client Credentials Grant** authentication flow client's credentials are used to identify the subject of authentication. If an account hasn't exit yet it will be created. 6 | 7 | *NOTE: the operation isn't allowed for disabled accounts* 8 | 9 | **URI** 10 | 11 | ``` 12 | POST /auth/${AUTH_KEY}/token 13 | ``` 14 | 15 | **URI parameters** 16 | 17 | Name | Type | Default | Description 18 | --------- | ------ | ---------- | ------------------ 19 | AUTH\_KEY | string | _required_ | Authentication key (follows `${PROTOCOL}.${PROVIDER}` convention) 20 | 21 | **Payload** 22 | 23 | Name | Type | Default | Description 24 | ------------- | ------ | ---------- | ------------------ 25 | grant\_type | string | _required_ | Always `client_credentials` 26 | client\_token | string | _required_ | Client credentials 27 | 28 | **Response** 29 | 30 | Name | Type | Default | Description 31 | -------------- | ------ | ---------- | ------------------ 32 | access\_token | string | _required_ | Used for account identification 33 | refresh\_token | string | _required_ | Used to refresh the access token, never expires 34 | expires\_in | string | _required_ | Expiration time of access token 35 | token\_type | string | _required_ | Always `Bearer` 36 | 37 | **Example** 38 | 39 | ```bash 40 | ## www-form payload 41 | curl -fsSL \ 42 | -XPOST ${ENDPOINT}/auth/oauth2.example/token \ 43 | -H 'Content-Type: application/x-www-form-urlencoded' \ 44 | -d "grant_type=client_credentials&client_token=${CLIENT_TOKEN}" \ 45 | | jq '.' 46 | 47 | ## JSON payload 48 | curl -fsSL \ 49 | -XPOST ${ENDPOINT}/auth/oauth2.example/token \ 50 | -H 'Content-Type: application/json' \ 51 | -d "{\"grant_type\":\"client_credentials\",\"client_token\":\"${CLIENT_TOKEN}\"}" \ 52 | | jq '.' 53 | 54 | { 55 | "access_token": "eyJhbGci...", 56 | "refresh_token": "eyJhbGci...", 57 | "expires_in": 86400, 58 | "token_type": "Bearer" 59 | } 60 | ``` 61 | 62 | 63 | 64 | ## Refresh access token 65 | 66 | Issues a new access token of account. A previously issued refresh token is used to identify the subject of authentication. 67 | 68 | *NOTE: the operation isn't allowed for disabled accounts* 69 | 70 | **URI** 71 | 72 | ``` 73 | POST /accounts/${KEY}/refresh 74 | ``` 75 | 76 | **URI parameters** 77 | 78 | Name | Type | Default | Description 79 | --------- | ------ | ---------- | ------------------ 80 | KEY | string | _required_ | Account identifier or `me` 81 | 82 | **Response** 83 | 84 | Name | Type | Default | Description 85 | -------------- | ------ | ---------- | ------------------ 86 | access\_token | string | _required_ | Used for account identification. 87 | expires\_in | string | _required_ | Expiration time of access token 88 | token\_type | string | _required_ | Always `Bearer` 89 | 90 | **Example** 91 | 92 | ```bash 93 | curl -fsSL \ 94 | -XPOST ${ENDPOINT}/accounts/me/refresh \ 95 | -H"Authorization: Bearer ${REFRESH_TOKEN}" \ 96 | | jq '.' 97 | 98 | { 99 | "access_token": "eyJhbGci...", 100 | "expires_in": 86400, 101 | "token_type": "Bearer" 102 | } 103 | ``` 104 | 105 | 106 | 107 | ## Revoke refresh token 108 | 109 | Revokes the old refresh token and issues a new one. 110 | 111 | *NOTE: the operation isn't allowed for disabled accounts* 112 | 113 | **URI** 114 | 115 | ``` 116 | POST /accounts/${KEY}/revoke 117 | ``` 118 | 119 | **URI parameters** 120 | 121 | Name | Type | Default | Description 122 | --------- | ------ | ---------- | ------------------ 123 | KEY | string | _required_ | Account identifier or `me` 124 | 125 | **Response** 126 | 127 | Name | Type | Default | Description 128 | -------------- | ------ | ---------- | ------------------ 129 | refresh\_token | string | _required_ | Used to refresh the access token, never expires 130 | 131 | **Example** 132 | 133 | ```bash 134 | curl -fsSL \ 135 | -XPOST ${ENDPOINT}/accounts/me/revoke \ 136 | -H"Authorization: Bearer ${REFRESH_TOKEN}" \ 137 | | jq '.' 138 | 139 | { 140 | "refresh_token": "eyJhbGci..." 141 | } 142 | ``` 143 | 144 | 145 | 146 | ## Add client's identity 147 | 148 | Add another client's identity to the account. 149 | 150 | *NOTE: the operation isn't allowed for disabled accounts* 151 | 152 | **URI** 153 | 154 | ``` 155 | POST /auth/${KEY}/link 156 | ``` 157 | 158 | **URI parameters** 159 | 160 | Name | Type | Default | Description 161 | --------- | ------ | ---------- | ------------------ 162 | KEY | string | _required_ | Account identifier or `me` 163 | 164 | **Payload** 165 | 166 | Name | Type | Default | Description 167 | ------------- | ------ | ---------- | ------------------ 168 | grant\_type | string | _required_ | Always `client_credentials` 169 | client\_token | string | _required_ | Client credentials 170 | 171 | **Response** 172 | 173 | Name | Type | Default | Description 174 | -------------- | ------ | ---------- | ------------------ 175 | id | string | _required_ | Client's identity identifier 176 | 177 | **Example** 178 | 179 | ```bash 180 | ## www-form payload 181 | curl -fsSL \ 182 | -XPOST ${ENDPOINT}/auth/oauth2.example/link \ 183 | -H "Authorization: Bearer ${ACCESS_TOKEN}" \ 184 | -H 'Content-Type: application/x-www-form-urlencoded' \ 185 | -d "grant_type=client_credentials&client_token=${CLIENT_TOKEN}" \ 186 | | jq '.' 187 | 188 | ## JSON payload 189 | curl -fsSL \ 190 | -XPOST ${ENDPOINT}/auth/oauth2.example/link \ 191 | -H "Authorization: Bearer ${ACCESS_TOKEN}" \ 192 | -H 'Content-Type: application/json' \ 193 | -d '{"grant_type":"client_credentials","client_token":"${CLIENT_TOKEN}"}' \ 194 | | jq '.' 195 | 196 | { 197 | "id": "oauth2.example.123" 198 | } 199 | ``` 200 | 201 | 202 | 203 | ## List client's identities 204 | 205 | Returns list of client's identities previously added to the account. 206 | 207 | **URI** 208 | 209 | ``` 210 | GET /accounts/${KEY}/auth 211 | ``` 212 | 213 | **URI parameters** 214 | 215 | Name | Type | Default | Description 216 | --------- | ------ | ---------- | ------------------ 217 | KEY | string | _required_ | Account identifier or `me` 218 | 219 | **Response** 220 | 221 | List of client's identities. 222 | 223 | **Example** 224 | 225 | ```bash 226 | curl -fsSL \ 227 | -XGET ${ENDPOINT}/accounts/me/auth \ 228 | -H"Authorization: Bearer ${ACCESS_TOKEN}" \ 229 | | jq '.' 230 | 231 | [ 232 | { 233 | "id": "oauth2.example.123" 234 | } 235 | ] 236 | ``` 237 | 238 | 239 | 240 | ## Delete client's identity 241 | 242 | Removes the client's identity. 243 | 244 | *NOTE: the operation isn't allowed for disabled accounts* 245 | 246 | **URI** 247 | 248 | ``` 249 | DELETE /accounts/${KEY}/auth/${IDENTITY} 250 | ``` 251 | 252 | **URI parameters** 253 | 254 | Name | Type | Default | Description 255 | --------- | ------ | ---------- | ------------------ 256 | KEY | string | _required_ | Account identifier or `me` 257 | IDENTITY | string | _required_ | Client's identity identifier 258 | 259 | **Response** 260 | 261 | Removed client's identity. 262 | 263 | **Example** 264 | 265 | ```bash 266 | curl -fsSL \ 267 | -XDELETE ${ENDPOINT}/accounts/me/auth/oauth2.example.123 \ 268 | -H"Authorization: Bearer ${ACCESS_TOKEN}" \ 269 | | jq '.' 270 | 271 | { 272 | "id": "oauth2.example.123" 273 | } 274 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | ARG RIAKKV_VERSION 4 | ARG ULIMIT_FD 5 | ENV RIAKKV_VERSION=${RIAKKV_VERSION:-2.2.0} 6 | ENV ULIMIT_FD=${ULIMIT_FD:-262144} 7 | 8 | ## ----------------------------------------------------------------------------- 9 | ## Installing dependencies 10 | ## ----------------------------------------------------------------------------- 11 | ENV DEBIAN_FRONTEND noninteractive 12 | RUN set -xe \ 13 | && apt-get update \ 14 | && apt-get -y --no-install-recommends install \ 15 | software-properties-common \ 16 | apt-transport-https \ 17 | ca-certificates \ 18 | lsb-release \ 19 | curl \ 20 | && apt-get update \ 21 | && apt-get -y --no-install-recommends install \ 22 | rsyslog \ 23 | vim-nox \ 24 | sudo \ 25 | less \ 26 | make \ 27 | g++ \ 28 | git \ 29 | jq 30 | 31 | ## ----------------------------------------------------------------------------- 32 | ## Installing Riak KV 33 | ## ----------------------------------------------------------------------------- 34 | RUN set -xe \ 35 | && add-apt-repository -s -y "deb https://packagecloud.io/basho/riak/ubuntu/ $(lsb_release -sc) main" \ 36 | && curl -fSL https://packagecloud.io/gpg.key 2>&1 | apt-key add -- \ 37 | && apt-get update \ 38 | && apt-get -y --no-install-recommends install \ 39 | riak=${RIAKKV_VERSION}-1 40 | 41 | ## ----------------------------------------------------------------------------- 42 | ## Configuring Riak KV 43 | ## ----------------------------------------------------------------------------- 44 | RUN set -xe \ 45 | && echo "ulimit -n ${ULIMIT_FD}" >> /etc/default/riak \ 46 | && perl -pi -e 's/(listener.http.internal = )127\.0\.0\.1/${1}0.0.0.0/' /etc/riak/riak.conf \ 47 | && perl -pi -e 's/(listener.protobuf.internal = )127\.0\.0\.1/${1}0.0.0.0/' /etc/riak/riak.conf \ 48 | && perl -pi -e 's/(?:(log.syslog = ).*)/${1}on/' /etc/riak/riak.conf 49 | 50 | ## ----------------------------------------------------------------------------- 51 | ## Enabling Riak Search 52 | ## ----------------------------------------------------------------------------- 53 | RUN set -xe \ 54 | && apt-get -y --no-install-recommends install \ 55 | default-jre-headless \ 56 | && perl -pi -e 's/(search = )off/${1}on/' /etc/riak/riak.conf 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016-2017 Andrei Nesterov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: update-schemas 2 | 3 | PROJECT = idp 4 | PROJECT_DESCRIPTION = Identity Provider. 5 | 6 | DEP_PLUGINS = \ 7 | version.mk 8 | 9 | DEPS = \ 10 | lager \ 11 | lager_syslog \ 12 | riakc_pool \ 13 | riakauth \ 14 | riakacl \ 15 | jose \ 16 | uuid \ 17 | cowboy \ 18 | exometer 19 | 20 | IGNORE_DEPS = \ 21 | folsom \ 22 | bear 23 | 24 | NO_AUTOPATCH = \ 25 | riak_pb 26 | 27 | dep_lager = git https://github.com/erlang-lager/lager.git 3.5.1 28 | dep_lager_syslog = git git://github.com/basho/lager_syslog.git 3.0.3 29 | dep_riakc_pool = git git://github.com/manifest/riak-connection-pool.git v0.2.1 30 | dep_riakauth = git git://github.com/manifest/riak-auth.git v0.1.3 31 | dep_riakacl = git git://github.com/manifest/riak-acl.git v0.2.0 32 | dep_jose = git git://github.com/manifest/jose-erlang.git v0.1.2 33 | dep_uuid = git git://github.com/okeuday/uuid.git v1.7.1 34 | dep_cowboy = git git://github.com/ninenines/cowboy.git 2.0.0-rc.2 35 | dep_exometer = git git://github.com/Feuerlabs/exometer.git 1.2.1 36 | 37 | BUILD_DEPS = version.mk 38 | dep_version.mk = git git://github.com/manifest/version.mk.git master 39 | 40 | TEST_DEPS = ct_helper gun 41 | dep_ct_helper = git git://github.com/ninenines/ct_helper.git master 42 | dep_gun = git git://github.com/manifest/gun.git feature/head-1xx 43 | 44 | SHELL_DEPS = tddreloader 45 | SHELL_OPTS = \ 46 | -eval 'application:ensure_all_started($(PROJECT), permanent)' \ 47 | -s tddreloader start \ 48 | -config rel/sys 49 | 50 | include erlang.mk 51 | 52 | GEN_IDP_ACCOUNT_SCHEMA_OUT = priv/riak-kv/schemas/idp_account.xml 53 | GEN_IDP_ACCOUNT_SCHEMA_SRC = deps/riakauth/priv/riak-kv/schemas/riakauth_account.xml 54 | GEN_IDP_ACCOUNT_ACLSUBJECT_SCHEMA_OUT = priv/riak-kv/schemas/idp_account_aclsubject.xml 55 | GEN_IDP_ACCOUNT_ACLSUBJECT_SCHEMA_SRC = deps/riakacl/priv/riak-kv/schemas/riakacl_subject.xml 56 | 57 | update-schemas: fetch-shell-deps 58 | $(verbose) cp $(GEN_IDP_ACCOUNT_SCHEMA_SRC) $(GEN_IDP_ACCOUNT_SCHEMA_OUT) 59 | $(verbose) cp $(GEN_IDP_ACCOUNT_ACLSUBJECT_SCHEMA_SRC) $(GEN_IDP_ACCOUNT_ACLSUBJECT_SCHEMA_OUT) 60 | 61 | export DEVELOP_ENVIRONMENT = $(shell if [ -f .develop-environment ]; then cat .develop-environment; fi) 62 | export EXOMETER_PACKAGES='(minimal)' 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Identity Provider 2 | 3 | [![Build Status][travis-img]][travis] 4 | 5 | Highly available, scalable and extendible Identity Provider. 6 | It utilises [OAuth2 Authorization Framework][rfc6749] to retrieve and associate 7 | one or many authentication identities (globally unique identifiers) 8 | with the unified account. 9 | 10 | At this point, only [OAuth2 Client Credentials Grant][rfc6749-client-credentials] flow is supported. 11 | 12 | 13 | 14 | ### How To Use 15 | 16 | To build and start playing with the application, 17 | execute following shell commands within different terminal tabs: 18 | 19 | ```bash 20 | ## Building the development image and running the container with Riak KV within it.. 21 | $ ./run-docker.sh 22 | ## Building the application and executing an erlang shell. 23 | $ make app shell 24 | ``` 25 | 26 | 27 | 28 | ### API 29 | 30 | IdP could be operated through its REST APIs: 31 | 32 | - [Authentication][api-authentication] 33 | - [Account][api-account] 34 | 35 | To make examples in the API reference work, we need to create an account with admin permissions (account that is a member of predefined `admin` ACL group). 36 | 37 | ```erlang 38 | %% We specify an account identifier explicitly just for simplicity reasons 39 | Tokens = 40 | idp_cli_account:create( 41 | #{acl => [{<<"admin">>, riakacl_group:new_dt()}]}, 42 | #{aud => <<"example.org">>, expires_in => infinity}), 43 | io:format( 44 | "ID='~s'~nACCESS_TOKEN='~s'~nREFRESH_TOKEN='~s'~n", 45 | [ maps:get(id, Tokens), 46 | maps:get(access_token, Tokens), 47 | maps:get(refresh_token, Tokens)]). 48 | ``` 49 | 50 | For authorization examples to work, we also need client's token. Here is how it can be created. 51 | 52 | ```erlang 53 | Claims = 54 | #{aud => <<"idp.example.org">>, 55 | iss => <<"example.org">>, 56 | exp => 32503680000, 57 | sub => <<"John">>}, 58 | {ok, Pem} = file:read_file(idp:conf_path(<<"keys/example.priv.pem">>)), 59 | {Alg, Priv} = jose_pem:parse_key(Pem), 60 | ClientToken = jose_jws_compact:encode(Claims, Alg, Priv), 61 | io:format("CLIENT_TOKEN='~s'~n", [ClientToken]). 62 | ``` 63 | 64 | Finally, we could use the following endpoint URI and tokens issued bellow. 65 | 66 | ```bash 67 | ENDPOINT='https://localhost:8443/api/v1' 68 | ``` 69 | 70 | 71 | 72 | ### License 73 | 74 | The source code is provided under the terms of [the MIT license][license]. 75 | 76 | [api-account]:https://github.com/foxford/idp/blob/master/API_ACCOUNT.md 77 | [api-authentication]:https://github.com/foxford/idp/blob/master/API_AUTHENTICATION.md 78 | [license]:http://www.opensource.org/licenses/MIT 79 | [rfc6749]:https://tools.ietf.org/html/rfc6749 80 | [rfc6749-client-credentials]:https://tools.ietf.org/html/rfc6749#section-4.4 81 | [travis]:https://travis-ci.org/foxford/idp?branch=master 82 | [travis-img]:https://secure.travis-ci.org/foxford/idp.png?branch=master 83 | -------------------------------------------------------------------------------- /ct-run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOCKER_CONTAINER_NAME='ct' 4 | DOCKER_EVENT_DONE='.docker.event.done' 5 | COMMON_TEST_OPTIONS=${COMMON_TEST_OPTIONS:-''} 6 | 7 | function CLEAN() { 8 | rm "${DOCKER_EVENT_DONE}" 2>/dev/null \ 9 | ; docker stop ${DOCKER_CONTAINER_NAME} 2>/dev/null \ 10 | ; docker rm ${DOCKER_CONTAINER_NAME} 2>/dev/null \ 11 | ; true 12 | } 13 | 14 | set -e 15 | CLEAN \ 16 | && DOCKER_RUN_OPTIONS="-dt --name ${DOCKER_CONTAINER_NAME}" \ 17 | DOCKER_CONTAINER_COMMAND="touch ${DOCKER_EVENT_DONE} && riak attach" \ 18 | ./run-docker.sh \ 19 | && while [ ! -f "${DOCKER_EVENT_DONE}" ]; do sleep 3; done \ 20 | && make ct ${COMMON_TEST_OPTIONS} 21 | CLEAN & 22 | -------------------------------------------------------------------------------- /priv/keys/example.priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MDECAQEEIONS5csWuzLVL4xcVN5UgtCulyt4m9hKORs6L+1MWWpMoAoGCCqGSM49 3 | AwEH 4 | -----END EC PRIVATE KEY----- 5 | 6 | -------------------------------------------------------------------------------- /priv/keys/example.pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIUiuL5c4nVJ78wZBB74Hk5h7nUnT 3 | aXSf7m/xyY5t8tSv1d03jM7Dzmth/wlPrMZc8D/2nHqIPseWcbREiNZu8w== 4 | -----END PUBLIC KEY----- 5 | 6 | -------------------------------------------------------------------------------- /priv/keys/idp.priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MDECAQEEIJmVfGpvoZfgBppuVtGOA4Iv7R4n6zkUfbune8sNVAGBoAoGCCqGSM49 3 | AwEH 4 | -----END EC PRIVATE KEY----- 5 | 6 | -------------------------------------------------------------------------------- /priv/keys/idp.pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEERmDx0BYu0Rb26JP6I8k6yqxQF7r 3 | /dUA7+4oLJ76PBKCZOnXk0PN+JktSuGrVTVZgOIsiYbsQN1mrgPBU0Jyww== 4 | -----END PUBLIC KEY----- 5 | 6 | -------------------------------------------------------------------------------- /priv/riak-kv/schemas/idp_account.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | _yz_id 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /priv/riak-kv/schemas/idp_account_aclsubject.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | _yz_id 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /priv/ssl/idp-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDPzCCAiegAwIBAgIJAKBENmHyiMMAMA0GCSqGSIb3DQEBCwUAMDYxFDASBgNV 3 | BAoMC2V4YW1wbGUub3JnMR4wHAYDVQQDDBVleGFtcGxlLm9yZyBBdXRob3JpdHkw 4 | HhcNMTcwODMxMTQyMjM4WhcNNDUwMTE2MTQyMjM4WjA2MRQwEgYDVQQKDAtleGFt 5 | cGxlLm9yZzEeMBwGA1UEAwwVZXhhbXBsZS5vcmcgQXV0aG9yaXR5MIIBIjANBgkq 6 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsETlDfG1vh9GMIR3FpLfQR3UHBfEoodc 7 | ax/TY9hpUaq/qjSlXC2aW0Oxo2+2NFVfBRJ0U387r3jrWSTWUpeyrpGCzQBDCTae 8 | rpVUJvfYCDw+GWgeDWYj/egLbX8SyVtgV5QFTV0djmuiLkms1fEiB7T4zbJSReux 9 | PInGEsS1gVNSGhZ8o1/WBRW85gchDigidiPmBxvcliv4HVhFq/N16pYmwOfKGMLI 10 | xi2nUSlkQjiiEVP2Q+2bhbKk/CKrDDv8+kgOIOLYjzFQ04K/fwBB+REWo3DY2U2p 11 | TsoXrbXMJmJpSfuClhef21FpfE7RPzoO/f9La+W7M8s+pmx53RP7NQIDAQABo1Aw 12 | TjAdBgNVHQ4EFgQUtUmlAdwIs0FMKD/Y/zFogveDFWowHwYDVR0jBBgwFoAUtUml 13 | AdwIs0FMKD/Y/zFogveDFWowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 14 | AQEAWF8hn7ae2+TTGECmYXOx7ovsfCmOthgust/cZVpA8DaRzWGx0HOeYNVLo/Gt 15 | Ib82z5KAJG9oVX1Nrj+HORjlXCsgog69AL33k1n4vy7LO2xroCDIFEYq/HIRr3FK 16 | sNJFJiAGdcGYJ3zhIuFQ3o+zQxLy2kWjMH67M00xT5mnn5BQbsa5A5e/67Hu/1Hy 17 | GiATi7TSsgv0ouK3glqpx1dsWhoGaT0kYEERMP4hCFn/T7Er1WKxW0ukMjIUNaNV 18 | QJXITP5w2I54bbdOTXjPr4PkYiMmx9QaXp0ik2zZYWbGS5SSIrk+GA53as35UufM 19 | TcCU7ROiFHQVmMTQjwzDPtPddg== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /priv/ssl/idp.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC4jCCAcoCCQDmJy//GrbeFzANBgkqhkiG9w0BAQsFADA2MRQwEgYDVQQKDAtl 3 | eGFtcGxlLm9yZzEeMBwGA1UEAwwVZXhhbXBsZS5vcmcgQXV0aG9yaXR5MB4XDTE3 4 | MDgzMTE0MjIzOVoXDTMxMDUxMDE0MjIzOVowMDEUMBIGA1UECgwLZXhhbXBsZS5v 5 | cmcxGDAWBgNVBAMMD2lkcC5leGFtcGxlLm9yZzCCASIwDQYJKoZIhvcNAQEBBQAD 6 | ggEPADCCAQoCggEBAKS17AmrfSPSCGL/LzEvmDL/ysOdn6me5pSDWPJhPbAecuws 7 | 4V9QTEuByNLSM7KVGzauIAQcPuyRktTIjHFXL1SxBHdf9NJaGxQemn+YVucF+aVU 8 | WiTTKFV+8pmbBrz+ucN2GQLINb2CKX6s1YUZTrYBOn+S3H6c5UinkJ3C0kcEq9hW 9 | n3iobQCOZiYflQqdk+XTyJV+WlaNhoZZufnrTdxs/+5RslVLnc/DGpaDO8F6epux 10 | yrHqbgtvmpI5NgCl5/6qUORHM785MyNHE1Z4L7GVOSEyiAMuvX8ORzSjJ/BWB57x 11 | pCr+LAXDJKDxK/K9eSJrUwClGJGsPPGSTERB0BMCAwEAATANBgkqhkiG9w0BAQsF 12 | AAOCAQEAPRB01fISnS1etIh7zWGG9h0jUGc6HNyxM2sQe+LOVS80jA9g9xCEGk7D 13 | aIuXdYXK9Gex1aU6B8w41Ia1WmJOZR8Ej0Y4Dy6rHff36Uh8F4BPPKJadDsx59N/ 14 | FUGEDdKiUq7joRbAYUNIA92BrqRvP40B6jsPZO3pTnD53CZ5ACKZB1JygiaJj+za 15 | 3YfBrBp7IqyxFxzmC/Im2ezPeDktppd41B0WEzhS9hGMDFMxzIBhyzH7FE53Mf49 16 | WKQjX3FMovFw/b+z9YULazlIIFPqOEtqTgq2bCF3mbUVI/L1aEUbeGb5e+gz2gBi 17 | QXEpgB9C+EQJafGEaUCuZa58SMZ1UA== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /priv/ssl/idp.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEApLXsCat9I9IIYv8vMS+YMv/Kw52fqZ7mlINY8mE9sB5y7Czh 3 | X1BMS4HI0tIzspUbNq4gBBw+7JGS1MiMcVcvVLEEd1/00lobFB6af5hW5wX5pVRa 4 | JNMoVX7ymZsGvP65w3YZAsg1vYIpfqzVhRlOtgE6f5LcfpzlSKeQncLSRwSr2Faf 5 | eKhtAI5mJh+VCp2T5dPIlX5aVo2Ghlm5+etN3Gz/7lGyVUudz8MaloM7wXp6m7HK 6 | sepuC2+akjk2AKXn/qpQ5EczvzkzI0cTVngvsZU5ITKIAy69fw5HNKMn8FYHnvGk 7 | Kv4sBcMkoPEr8r15ImtTAKUYkaw88ZJMREHQEwIDAQABAoIBACoic6BBo0CPkR+q 8 | AeuGLlwVfUwvPVHJ2VhyhUVzxLESCPoLvReB1hKWv8XWie0MIasKPhxvEVW5I6OV 9 | LgAsemoi4m6bOGi7LiQmdAVh9hEhQSye+HRlI/NDB1JuCvo7+09aHanNh5nT+0Nx 10 | eSmUQMNkaw+JhShKgIjmfKMu3FXYHc0YqKVL2yEdMEO7iFSZeycR8iia3zgYRYcu 11 | 9Wq+plWkb5PuXqaoD+ckTPsds4xxLoYeCGON42wFW3JRnybpuKJjCHzoz0QaRle6 12 | V9uFN1BzuQ/x80TAL6GoeBxycXxEKiGp8iJxoGSNolY4L+EixCc2y1ywFOUyu+2f 13 | G/hXXmkCgYEA2rQtjtCA8ay4BnnrRxBELFAGAkq5xBb3HP76kFISWXAfIuj+Yn2G 14 | bC3W/FHdhILsd04p4WoAMYeueOD1lD2xfnToVAGsDAyN3ijbPpbJC/Fm7YG7/2uo 15 | wxwZ7Mi6nnPcJ6iD2WmvNGJXVFGBNwDzE1DmlUxuoU8GgGg8np2G2y8CgYEAwMyY 16 | gkAY8DQtkwq2fq/ZqQ3atGcQBd82L+DBHCldpikLMQul4fXPgf6y/dvqd06bhvxo 17 | nZBSFl8vtYpmtwY8YuD4IwfA0sC1liZvExkOyvn9/SQBcM5R8pnstN3QOngJsi+g 18 | 9uzNAfVEdv+0hGZ9GNfXT9hJ7RQkE+fLM6cy0F0CgYEA1167xnn5oQTvrCD/2tlf 19 | 6Stc34Dq8vmSnBFUei74Nu89GknLyP3IFFwH7C5KMKYla0+j2oFic2QkIpGWBUfD 20 | tL431BJZdPwf8PjW/wnKLmKpc5ZgpiVE6e6QcScy77s0wDEotj9m8/Ur/rLMxne+ 21 | 5/SxPbEo+N0zj9wWZjTGiq0CgYABZnsFFyoXNInQM5e3u9c83xjjjowTPtfJ6Tv9 22 | 1F8Vwd6O8KK3zW1AaHUsWtiNHUkL5fFsk4vFFdPm4aZ1VdpCbZffyUKhRT0MZiMQ 23 | ZHIzDzXFDOnlw9nchTmu5p2Ijy6i2K22nWmvxfRFWP4aqBPohkjOD6gZzLemXVyg 24 | d2prEQKBgH4wCLd8zJeB34/1y71hntnvcuBfBwqeJjvSNOuaZT498ZW5wo9UUWGc 25 | CxXldcP1hOLUxXQ+faJyyWmLhTJDjgzdn+zhq7kmrd2htfXq/i73KukkE5zF3CFQ 26 | 3XlQgy03qDhTTQOlm4n/C7lmWUQghECoJ51LEeew71v4Kr1/Nnbb 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /priv/ssl/idp.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC4jCCAcoCCQDmJy//GrbeFzANBgkqhkiG9w0BAQsFADA2MRQwEgYDVQQKDAtl 3 | eGFtcGxlLm9yZzEeMBwGA1UEAwwVZXhhbXBsZS5vcmcgQXV0aG9yaXR5MB4XDTE3 4 | MDgzMTE0MjIzOVoXDTMxMDUxMDE0MjIzOVowMDEUMBIGA1UECgwLZXhhbXBsZS5v 5 | cmcxGDAWBgNVBAMMD2lkcC5leGFtcGxlLm9yZzCCASIwDQYJKoZIhvcNAQEBBQAD 6 | ggEPADCCAQoCggEBAKS17AmrfSPSCGL/LzEvmDL/ysOdn6me5pSDWPJhPbAecuws 7 | 4V9QTEuByNLSM7KVGzauIAQcPuyRktTIjHFXL1SxBHdf9NJaGxQemn+YVucF+aVU 8 | WiTTKFV+8pmbBrz+ucN2GQLINb2CKX6s1YUZTrYBOn+S3H6c5UinkJ3C0kcEq9hW 9 | n3iobQCOZiYflQqdk+XTyJV+WlaNhoZZufnrTdxs/+5RslVLnc/DGpaDO8F6epux 10 | yrHqbgtvmpI5NgCl5/6qUORHM785MyNHE1Z4L7GVOSEyiAMuvX8ORzSjJ/BWB57x 11 | pCr+LAXDJKDxK/K9eSJrUwClGJGsPPGSTERB0BMCAwEAATANBgkqhkiG9w0BAQsF 12 | AAOCAQEAPRB01fISnS1etIh7zWGG9h0jUGc6HNyxM2sQe+LOVS80jA9g9xCEGk7D 13 | aIuXdYXK9Gex1aU6B8w41Ia1WmJOZR8Ej0Y4Dy6rHff36Uh8F4BPPKJadDsx59N/ 14 | FUGEDdKiUq7joRbAYUNIA92BrqRvP40B6jsPZO3pTnD53CZ5ACKZB1JygiaJj+za 15 | 3YfBrBp7IqyxFxzmC/Im2ezPeDktppd41B0WEzhS9hGMDFMxzIBhyzH7FE53Mf49 16 | WKQjX3FMovFw/b+z9YULazlIIFPqOEtqTgq2bCF3mbUVI/L1aEUbeGb5e+gz2gBi 17 | QXEpgB9C+EQJafGEaUCuZa58SMZ1UA== 18 | -----END CERTIFICATE----- 19 | -----BEGIN CERTIFICATE----- 20 | MIIDPzCCAiegAwIBAgIJAKBENmHyiMMAMA0GCSqGSIb3DQEBCwUAMDYxFDASBgNV 21 | BAoMC2V4YW1wbGUub3JnMR4wHAYDVQQDDBVleGFtcGxlLm9yZyBBdXRob3JpdHkw 22 | HhcNMTcwODMxMTQyMjM4WhcNNDUwMTE2MTQyMjM4WjA2MRQwEgYDVQQKDAtleGFt 23 | cGxlLm9yZzEeMBwGA1UEAwwVZXhhbXBsZS5vcmcgQXV0aG9yaXR5MIIBIjANBgkq 24 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsETlDfG1vh9GMIR3FpLfQR3UHBfEoodc 25 | ax/TY9hpUaq/qjSlXC2aW0Oxo2+2NFVfBRJ0U387r3jrWSTWUpeyrpGCzQBDCTae 26 | rpVUJvfYCDw+GWgeDWYj/egLbX8SyVtgV5QFTV0djmuiLkms1fEiB7T4zbJSReux 27 | PInGEsS1gVNSGhZ8o1/WBRW85gchDigidiPmBxvcliv4HVhFq/N16pYmwOfKGMLI 28 | xi2nUSlkQjiiEVP2Q+2bhbKk/CKrDDv8+kgOIOLYjzFQ04K/fwBB+REWo3DY2U2p 29 | TsoXrbXMJmJpSfuClhef21FpfE7RPzoO/f9La+W7M8s+pmx53RP7NQIDAQABo1Aw 30 | TjAdBgNVHQ4EFgQUtUmlAdwIs0FMKD/Y/zFogveDFWowHwYDVR0jBBgwFoAUtUml 31 | AdwIs0FMKD/Y/zFogveDFWowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 32 | AQEAWF8hn7ae2+TTGECmYXOx7ovsfCmOthgust/cZVpA8DaRzWGx0HOeYNVLo/Gt 33 | Ib82z5KAJG9oVX1Nrj+HORjlXCsgog69AL33k1n4vy7LO2xroCDIFEYq/HIRr3FK 34 | sNJFJiAGdcGYJ3zhIuFQ3o+zQxLy2kWjMH67M00xT5mnn5BQbsa5A5e/67Hu/1Hy 35 | GiATi7TSsgv0ouK3glqpx1dsWhoGaT0kYEERMP4hCFn/T7Er1WKxW0ukMjIUNaNV 36 | QJXITP5w2I54bbdOTXjPr4PkYiMmx9QaXp0ik2zZYWbGS5SSIrk+GA53as35UufM 37 | TcCU7ROiFHQVmMTQjwzDPtPddg== 38 | -----END CERTIFICATE----- 39 | -------------------------------------------------------------------------------- /rel/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {idp, [ 3 | ]}, 4 | {lager, [ 5 | {error_logger_redirect, false} 6 | ]}, 7 | {exometer, [ 8 | {predefined, [ 9 | %% http 10 | {[idp,pool,http], { 11 | function, erlang, length, [{'$call', ranch, procs, [httpd, '$dp']}], 12 | value, [acceptors, connections] 13 | }, []}, 14 | {[idp,request,http,duration], histogram, []}, 15 | {[idp,request,http,count], spiral, [{time_span, 60000}]}, 16 | %% riak kv 17 | {[idp,pool,kv_protobuf], { 18 | function, poolboy, status, [kv_protobuf], 19 | match, {'_',idle,'_',busy} 20 | }, []}, 21 | %% erlang vm 22 | {[idp,memory], { 23 | function, erlang, memory, [], 24 | proplist, [total, processes, processes_used, system, atom, atom_used, binary, ets] 25 | }, []}, 26 | {[idp,io], { 27 | function, erlang, statistics, [io], 28 | match, {{'_',in},{'_',out}} 29 | }, []}, 30 | {[idp,system_info], 31 | {function, erlang, system_info, ['$dp'], 32 | value, [port_count,port_limit,process_count,process_limit,thread_pool_size] 33 | }, []} 34 | ]}, 35 | {report, [ 36 | {subscribers, [ 37 | %% http 38 | {exometer_report_statsd, [idp,pool,http], acceptors, 10000, true}, 39 | {exometer_report_statsd, [idp,pool,http], connections, 10000, true}, 40 | {exometer_report_statsd, [idp,request,http,duration], max, 10000, true}, 41 | {exometer_report_statsd, [idp,request,http,duration], min, 10000, true}, 42 | {exometer_report_statsd, [idp,request,http,duration], mean, 10000, true}, 43 | {exometer_report_statsd, [idp,request,http,duration], median, 60000, true}, 44 | {exometer_report_statsd, [idp,request,http,duration], 75, 60000, true}, 45 | {exometer_report_statsd, [idp,request,http,duration], 90, 60000, true}, 46 | {exometer_report_statsd, [idp,request,http,duration], n, 60000, true}, 47 | {exometer_report_statsd, [idp,request,http,count], one, 60000, true}, 48 | %% riak kv 49 | {exometer_report_statsd, [idp,pool,kv_protobuf], idle, 10000, true}, 50 | {exometer_report_statsd, [idp,pool,kv_protobuf], busy, 10000, true}, 51 | %% erlang vm 52 | {exometer_report_statsd, [idp,memory], total, 10000, true}, 53 | {exometer_report_statsd, [idp,memory], processes, 10000, true}, 54 | {exometer_report_statsd, [idp,memory], processes_used, 10000, true}, 55 | {exometer_report_statsd, [idp,memory], system, 10000, true}, 56 | {exometer_report_statsd, [idp,memory], atom, 10000, true}, 57 | {exometer_report_statsd, [idp,memory], atom_used, 10000, true}, 58 | {exometer_report_statsd, [idp,memory], binary, 10000, true}, 59 | {exometer_report_statsd, [idp,memory], ets, 10000, true}, 60 | {exometer_report_statsd, [idp,io], in, 10000, true}, 61 | {exometer_report_statsd, [idp,io], out, 10000, true}, 62 | {exometer_report_statsd, [idp,system_info], port_count, 10000, true}, 63 | {exometer_report_statsd, [idp,system_info], port_limit, 60000, true}, 64 | {exometer_report_statsd, [idp,system_info], process_count, 10000, true}, 65 | {exometer_report_statsd, [idp,system_info], process_limit, 60000, true}, 66 | {exometer_report_statsd, [idp,system_info], thread_pool_size, 10000, true} 67 | ]}, 68 | {reporters, [ 69 | {exometer_report_statsd, [ 70 | {hostname, "localhost"}, 71 | {port, 8125} 72 | ]} 73 | ]} 74 | ]} 75 | ]} 76 | ]. 77 | 78 | -------------------------------------------------------------------------------- /rel/vm.args: -------------------------------------------------------------------------------- 1 | -name idp@127.0.0.1 2 | -setcookie idp 3 | -heart 4 | -------------------------------------------------------------------------------- /relx.config: -------------------------------------------------------------------------------- 1 | {release, {idp, "1"}, [sasl, idp]}. 2 | {include_src, false}. 3 | {extended_start_script, true}. 4 | {sys_config, "rel/sys.config"}. 5 | {vm_args, "rel/vm.args"}. 6 | 7 | -------------------------------------------------------------------------------- /run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT='idp' 4 | PROJECT_DIR="/opt/sandbox/${PROJECT}" 5 | DOCKER_CONTAINER_NAME="sandbox/${PROJECT}" 6 | DOCKER_CONTAINER_COMMAND=${DOCKER_CONTAINER_COMMAND:-'/bin/bash'} 7 | DOCKER_RUN_OPTIONS=${DOCKER_RUN_OPTIONS:-'-ti --rm'} 8 | DOCKER_RIAKKV_PROTOBUF_PORT=${DOCKER_RIAKKV_PROTOBUF_PORT:-8087} 9 | DOCKER_RIAKKV_HTTP_PORT=${DOCKER_RIAKKV_HTTP_PORT:-8098} 10 | DEVELOP_ENVIRONMENT='.develop-environment' 11 | ULIMIT_FD=262144 12 | 13 | function CREATE_DEVELOP_ENVIRONMENT() { 14 | local DOCKER_MACHINE_IP=$(docker-machine ip) 15 | local DOCKER_IP=${DOCKER_MACHINE_IP:-'localhost'} 16 | printf \ 17 | "#{kv_protobuf => #{host => \"%s\", port => %s}, kv_http => #{host => \"%s\", port => %s}}." \ 18 | "${DOCKER_IP}" "${DOCKER_RIAKKV_PROTOBUF_PORT}" \ 19 | "${DOCKER_IP}" "${DOCKER_RIAKKV_HTTP_PORT}" \ 20 | > "${DEVELOP_ENVIRONMENT}" 21 | } 22 | 23 | function PROPS() { 24 | local INDEX_NAME="${1}" 25 | local BUCKET_OPTIONS="${2}" 26 | if [[ ${BUCKET_OPTIONS} ]]; then 27 | echo "{\"props\":{\"search_index\":\"${INDEX_NAME}\",${BUCKET_OPTIONS}}}" 28 | else 29 | echo "{\"props\":{\"search_index\":\"${INDEX_NAME}\"}}" 30 | fi 31 | } 32 | 33 | function CREATE_TYPE() { 34 | local HOST='http://localhost:8098' 35 | local SCHEMA_NAME="${1}" 36 | local INDEX_NAME="${1}_idx" 37 | local TYPE_NAME="${1}_t" 38 | local BUCKET_OPTIONS="${2}" 39 | read -r RESULT <<-EOF 40 | curl -fSL \ 41 | -XPUT "${HOST}/search/schema/${SCHEMA_NAME}" \ 42 | -H 'Content-Type: application/xml' \ 43 | --data-binary @"${PROJECT_DIR}/priv/riak-kv/schemas/${SCHEMA_NAME}.xml" \ 44 | && curl -fSL \ 45 | -XPUT "${HOST}/search/index/${INDEX_NAME}" \ 46 | -H 'Content-Type: application/json' \ 47 | -d '{"schema":"${SCHEMA_NAME}"}' \ 48 | && riak-admin bucket-type create ${TYPE_NAME} '$(PROPS ${INDEX_NAME} ${BUCKET_OPTIONS})' \ 49 | && riak-admin bucket-type activate ${TYPE_NAME} 50 | EOF 51 | echo "${RESULT}" 52 | } 53 | 54 | read -r DOCKER_RUN_COMMAND <<-EOF 55 | service rsyslog start \ 56 | && riak start \ 57 | && riak-admin wait-for-service riak_kv \ 58 | && $(CREATE_TYPE idp_account '"datatype":"map"') \ 59 | && $(CREATE_TYPE idp_account_aclsubject '"datatype":"map"') 60 | EOF 61 | 62 | CREATE_DEVELOP_ENVIRONMENT 63 | docker build -t ${DOCKER_CONTAINER_NAME} . 64 | docker run ${DOCKER_RUN_OPTIONS} \ 65 | -v $(pwd):${PROJECT_DIR} \ 66 | --ulimit nofile=${ULIMIT_FD}:${ULIMIT_FD} \ 67 | -p ${DOCKER_RIAKKV_PROTOBUF_PORT}:8087 \ 68 | -p ${DOCKER_RIAKKV_HTTP_PORT}:8098 \ 69 | ${DOCKER_CONTAINER_NAME} \ 70 | /bin/bash -c "set -x && cd ${PROJECT_DIR} && ${DOCKER_RUN_COMMAND} && set +x && ${DOCKER_CONTAINER_COMMAND}" 71 | -------------------------------------------------------------------------------- /src/idp.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp). 26 | 27 | %% API 28 | -export([ 29 | unix_time/0, 30 | unix_time/1, 31 | unix_time_us/0, 32 | unix_time_us/1, 33 | make_uuid/0, 34 | priv_path/1, 35 | conf_path/1, 36 | authorize_subject/3, 37 | authorize_admin/3, 38 | decode_access_token/2, 39 | decode_refresh_token/3 40 | ]). 41 | 42 | %% Configuration 43 | -export([ 44 | http_options/0, 45 | httpd_options/0, 46 | allowed_origins/0, 47 | preflight_request_max_age/0, 48 | riak_connection_pools/0, 49 | identity_providers/0, 50 | tokens/0, 51 | authentication/0, 52 | resources/0 53 | ]). 54 | 55 | %% Definitions 56 | -define(APP, ?MODULE). 57 | 58 | %% ============================================================================= 59 | %% API 60 | %% ============================================================================= 61 | 62 | -spec unix_time() -> non_neg_integer(). 63 | unix_time() -> 64 | unix_time(erlang:timestamp()). 65 | 66 | -spec unix_time(erlang:timestamp()) -> non_neg_integer(). 67 | unix_time({MS, S, _US}) -> 68 | MS * 1000000 + S. 69 | 70 | -spec unix_time_us() -> non_neg_integer(). 71 | unix_time_us() -> 72 | unix_time_us(erlang:timestamp()). 73 | 74 | -spec unix_time_us(erlang:timestamp()) -> non_neg_integer(). 75 | unix_time_us({MS, S, US}) -> 76 | MS * 1000000000000 + S * 1000000 + US. 77 | 78 | -spec make_uuid() -> binary(). 79 | make_uuid() -> 80 | uuid:uuid_to_string(uuid:get_v4(), binary_standard). 81 | 82 | -spec priv_path(binary()) -> binary(). 83 | priv_path(Path) -> 84 | Priv = 85 | case code:priv_dir(?APP) of 86 | {error, _} -> "priv"; 87 | Dir -> Dir 88 | end, 89 | <<(list_to_binary(Priv))/binary, $/, Path/binary>>. 90 | 91 | -spec conf_path(binary()) -> binary(). 92 | conf_path(Path) -> 93 | case filename:pathtype(Path) of 94 | relative -> priv_path(Path); 95 | _ -> Path 96 | end. 97 | 98 | -spec authorize_subject(binary(), map(), map()) -> {ok, binary(), map()} | {error, any()}. 99 | authorize_subject(Akey, AuthM, Rdesc) -> 100 | #{account_aclsubject := #{bucket := Sb, pool := KVpool}, 101 | admin_aclgroup := AdminGroupName} = Rdesc, 102 | 103 | OwnerRW = AdminRW = #{read => true, write => true}, 104 | AdminAccess = {AdminGroupName, AdminRW}, 105 | KVpid = riakc_pool:lock(KVpool), 106 | Result = 107 | case maps:find(<<"sub">>, AuthM) of 108 | {ok, Skey} when Akey =:= <<"me">> -> {ok, Skey, OwnerRW}; 109 | {ok, Skey} when Akey =:= Skey -> {ok, Akey, OwnerRW}; 110 | {ok, Skey} -> 111 | case riakacl:authorize_predefined_object(KVpid, Sb, Skey, [AdminAccess], riakacl_rwaccess) of 112 | {ok, RW} -> {ok, Akey, RW}; 113 | Err -> Err 114 | end; 115 | _ -> 116 | {error, missing_aclsubject_key} 117 | end, 118 | riakc_pool:unlock(KVpool, KVpid), 119 | Result. 120 | 121 | -spec authorize_admin(binary(), map(), map()) -> {ok, binary(), map()} | {error, any()}. 122 | authorize_admin(Akey, AuthM, Rdesc) -> 123 | #{account_aclsubject := #{bucket := Sb, pool := KVpool}, 124 | admin_aclgroup := AdminGroupName} = Rdesc, 125 | 126 | AdminRW = #{read => true, write => true}, 127 | AdminAccess = {AdminGroupName, AdminRW}, 128 | KVpid = riakc_pool:lock(KVpool), 129 | Result = 130 | case maps:find(<<"sub">>, AuthM) of 131 | {ok, Skey} -> 132 | case riakacl:authorize_predefined_object(KVpid, Sb, Skey, [AdminAccess], riakacl_rwaccess) of 133 | {ok, RW} -> 134 | case Akey of 135 | <<"me">> -> {ok, Skey, RW}; 136 | _ -> {ok, Akey, RW} 137 | end; 138 | Err -> 139 | Err 140 | end; 141 | _ -> 142 | {error, missing_aclsubject_key} 143 | end, 144 | riakc_pool:unlock(KVpool, KVpid), 145 | Result. 146 | 147 | -spec decode_access_token(binary(), map()) -> map(). 148 | decode_access_token(Token, AuthConf) -> 149 | jose_jws_compact:decode_fn( 150 | fun(Data, _Opts) -> select_authentication_key(Data, AuthConf) end, 151 | Token). 152 | 153 | -spec decode_refresh_token(binary(), map(), map()) -> map(). 154 | decode_refresh_token(Token, _AuthConf, Rdesc) -> 155 | jose_jws_compact:decode_fn( 156 | fun([ _, #{<<"sub">> := Akey} | _ ], Opts) -> 157 | #{account := #{bucket := Ab, pool := KVpool}} = Rdesc, 158 | KVpid = riakc_pool:lock(KVpool), 159 | MaybeA = riakauth_account:find(KVpid, Ab, Akey), 160 | riakc_pool:unlock(KVpool, KVpid), 161 | 162 | case MaybeA of 163 | {ok, A} -> 164 | case idp_account:refresh_token_dt(A) of 165 | #{alg := RefreshAlg, key := RefreshKey} -> {ok, {RefreshAlg, RefreshKey, Opts}}; 166 | _ -> {error, missing_refresh_token} 167 | end; 168 | _ -> 169 | {error, bad_account_key} 170 | end 171 | end, 172 | Token). 173 | 174 | %% ============================================================================= 175 | %% Configuration 176 | %% ============================================================================= 177 | 178 | -spec http_options() -> list(). 179 | http_options() -> 180 | Default = 181 | [ {port, 8443}, 182 | {certfile, conf_path(<<"ssl/idp.crt">>)}, 183 | {keyfile, conf_path(<<"ssl/idp.key">>)} ], 184 | application:get_env(?APP, ?FUNCTION_NAME, Default). 185 | 186 | -spec httpd_options() -> map(). 187 | httpd_options() -> 188 | application:get_env(?APP, ?FUNCTION_NAME, #{}). 189 | 190 | -spec allowed_origins() -> Origin | [Origin] | '*' when Origin :: {binary(), binary(), 0..65535}. 191 | allowed_origins() -> 192 | application:get_env(?APP, ?FUNCTION_NAME, '*'). 193 | 194 | -spec preflight_request_max_age() -> binary(). 195 | preflight_request_max_age() -> 196 | application:get_env(?APP, ?FUNCTION_NAME, <<"0">>). 197 | 198 | -spec riak_connection_pools() -> [map()]. 199 | riak_connection_pools() -> 200 | case application:get_env(?APP, ?FUNCTION_NAME) of 201 | {ok, Val} -> Val; 202 | _ -> 203 | %% Getting default values from the Docker environment 204 | %% configuration file, if it's available. 205 | try 206 | {ok, S, _} = erl_scan:string(os:getenv("DEVELOP_ENVIRONMENT")), 207 | {ok, Conf} = erl_parse:parse_term(S), 208 | #{kv_protobuf := #{host := Host, port := Port}} = Conf, 209 | [ #{name => kv_protobuf, 210 | size => 5, 211 | connection => 212 | #{host => Host, 213 | port => Port, 214 | options => [queue_if_disconnected]}} ] 215 | catch _:Reason -> error({missing_develop_environment, ?FUNCTION_NAME, Reason}) end 216 | end. 217 | 218 | -spec identity_providers() -> list(). 219 | identity_providers() -> 220 | DevelopConf = 221 | [ %% oauth2.example 222 | #{key => [<<"oauth2">>, <<"example">>], 223 | options => 224 | #{access => #{admin => true, moderator => true}, 225 | keyfile => conf_path(<<"keys/example.pub.pem">>), 226 | verify_options => #{verify => [exp, {iss, <<"example.org">>}]}}}, 227 | %% oauth2.example-restricted 228 | #{key => [<<"oauth2">>, <<"example-restricted">>], 229 | options => 230 | #{access => #{}, 231 | keyfile => conf_path(<<"keys/example.pub.pem">>), 232 | verify_options => #{verify => [exp, {iss, <<"example.org">>}]}}} ], 233 | Default = [], 234 | case application:get_env(?APP, ?FUNCTION_NAME) of 235 | {ok, Val} -> Val; 236 | _ -> 237 | %% Getting development values, if environment variable is defined. 238 | case os:getenv("DEVELOP_ENVIRONMENT") of 239 | Env when Env =:= false; Env =:= [] -> Default; 240 | _ -> DevelopConf 241 | end 242 | end. 243 | 244 | -spec tokens() -> map(). 245 | tokens() -> 246 | DevelopConf = 247 | #{type => <<"Bearer">>, 248 | expires_in => 600, %% 10 minutes 249 | iss => <<"idp.example.org">>, 250 | aud => <<"app.example.org">>, 251 | access_token => #{keyfile => conf_path(<<"keys/idp.priv.pem">>)}, 252 | refresh_token => #{alg => <<"HS256">>}}, 253 | M = 254 | case application:get_env(?APP, ?FUNCTION_NAME) of 255 | {ok, Val} -> Val; 256 | _ -> 257 | %% Getting development values, if environment variable is defined. 258 | case os:getenv("DEVELOP_ENVIRONMENT") of 259 | Env when Env =:= false; Env =:= [] -> error(missing_access_token_options_config); 260 | _ -> DevelopConf 261 | end 262 | end, 263 | M#{access_token => load_auth_key(maps:get(access_token, M))}. 264 | 265 | -spec authentication() -> map(). 266 | authentication() -> 267 | %% Examples: 268 | %% #{<<"iss">> => 269 | %% #{keyfile => <<"keys/example.pem">>, 270 | %% verify_options => DefaultVerifyOpts}} 271 | %% #{{<<"iss">>, <<"kid">>} => 272 | %% #{keyfile => <<"keys/example.pem">>, 273 | %% verify_options => DefaultVerifyOpts}} 274 | DevelopConf = 275 | #{<<"idp.example.org">> => 276 | #{keyfile => conf_path(<<"keys/idp.pub.pem">>), 277 | verify_options => #{verify => [exp]}}}, 278 | DefaultVerifyOpts = 279 | #{parse_header => map, 280 | parse_payload => map, 281 | parse_signature => binary, 282 | verify => [exp, nbf, iat], 283 | leeway => 1}, 284 | Default = #{}, 285 | M = 286 | case application:get_env(?APP, ?FUNCTION_NAME) of 287 | {ok, Val} -> Val; 288 | _ -> 289 | %% Getting development values, if environment variable is defined. 290 | case os:getenv("DEVELOP_ENVIRONMENT") of 291 | Env when Env =:= false; Env =:= [] -> Default; 292 | _ -> DevelopConf 293 | end 294 | end, 295 | try configure_auth(M, DefaultVerifyOpts) 296 | catch throw:R -> error({invalid_authentication_config, R, M}) end. 297 | 298 | -spec resources() -> map(). 299 | resources() -> 300 | Default = 301 | #{account => 302 | #{pool => kv_protobuf, 303 | bucket => {<<"idp_account_t">>, <<"idp-account">>}, 304 | index => <<"idp_account_idx">>, 305 | handler => idp_accounth_stub}, 306 | account_aclsubject => 307 | #{pool => kv_protobuf, 308 | bucket => {<<"idp_account_aclsubject_t">>, <<"idp-account-aclsubject">>}}, 309 | anonymous_aclgroup => <<"anonymous">>, 310 | admin_aclgroup => <<"admin">>}, 311 | application:get_env(?APP, ?FUNCTION_NAME, Default). 312 | 313 | %% ============================================================================= 314 | %% Internal functions 315 | %% ============================================================================= 316 | 317 | -spec configure_auth(map(), map()) -> map(). 318 | configure_auth(M, DefaultVerifyOpts) -> 319 | maps:map( 320 | fun(_Iss, Conf) -> 321 | load_auth_key(Conf#{verify_options => maps:merge(DefaultVerifyOpts, maps:get(verify_options, Conf, #{}))}) 322 | end, M). 323 | 324 | -spec load_auth_key(map()) -> map(). 325 | load_auth_key(#{alg := _, key := _} =M) -> M; 326 | load_auth_key(#{keyfile := Path} =M) -> 327 | try 328 | {ok, Pem} = file:read_file(conf_path(Path)), 329 | {Alg, Key} = jose_pem:parse_key(Pem), 330 | M#{alg => Alg, key => Key} 331 | catch _:_ -> 332 | throw({bad_keyfile, Path}) 333 | end; 334 | load_auth_key(_) -> 335 | throw(missing_key). 336 | 337 | -spec select_authentication_key(list(), map()) -> jose_jws_compact:select_key_result(). 338 | select_authentication_key([ _, #{<<"iss">> := Iss} | _ ], Conf) -> 339 | select_authentication_config(Iss, Conf); 340 | select_authentication_key([ #{<<"kid">> := Kid}, #{<<"iss">> := Iss} | _ ], Conf) -> 341 | select_authentication_config({Iss, Kid}, Conf); 342 | select_authentication_key(_Data, _Conf) -> 343 | {error, missing_access_token_iss}. 344 | 345 | -spec select_authentication_config(binary() | {binary(), binary()}, map()) -> jose_jws_compact:select_key_result(). 346 | select_authentication_config(IssKid, Conf) -> 347 | case maps:find(IssKid, Conf) of 348 | {ok, M} -> 349 | #{alg := Alg, key := Key, verify_options := Opts} = M, 350 | {ok, {Alg, Key, Opts}}; 351 | _ -> 352 | {error, {missing_authentication_config, IssKid}} 353 | end. 354 | -------------------------------------------------------------------------------- /src/idp_account.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_account). 26 | 27 | %% CRUD API 28 | -export([ 29 | refresh_access_token/3, 30 | revoke_refresh_token/3, 31 | create/4, 32 | link/5, 33 | read/2, 34 | read/3, 35 | delete/3, 36 | enable/3, 37 | disable/3 38 | ]). 39 | 40 | %% API 41 | -export([ 42 | is_enabled/1, 43 | to_map/2 44 | ]). 45 | 46 | %% DataType API 47 | -export([ 48 | update_enabled_dt/2, 49 | refresh_token_dt/1, 50 | update_refresh_token_dt/4, 51 | fold_refresh_token_dt/3, 52 | generate_refresh_token_key/1 53 | ]). 54 | 55 | %% ============================================================================= 56 | %% CRUD API 57 | %% ============================================================================= 58 | 59 | -spec refresh_access_token(binary(), map(), map()) -> map(). 60 | refresh_access_token(Akey, Tokens, Rdesc) -> 61 | #{account := 62 | #{pool := Pool, 63 | bucket := Ab}} = Rdesc, 64 | #{type := Type, 65 | expires_in := ExpiresIn, 66 | iss := Iss, 67 | aud := Aud, 68 | access_token := 69 | #{alg := Alg, 70 | key := Key}} = Tokens, 71 | 72 | KVpid = riakc_pool:lock(Pool), 73 | A = riakauth_account:get(KVpid, Ab, Akey), 74 | riakc_pool:unlock(Pool, KVpid), 75 | 76 | %% We must not issue any tokens for disabled accounts. 77 | true = is_enabled(A), 78 | 79 | Now = idp:unix_time(), 80 | Exp = Now +ExpiresIn, 81 | 82 | AccessToken = 83 | jose_jws_compact:encode( 84 | #{iss => Iss, 85 | aud => Aud, 86 | sub => Akey, 87 | exp => Exp}, 88 | Alg, 89 | Key), 90 | 91 | %% According to RFC 6749 - The OAuth 2.0 Authorization Framework 92 | %% 5.1. Issuing an Access Token. Successful Response 93 | %% https://tools.ietf.org/html/rfc6749#section-5.1 94 | #{access_token => AccessToken, 95 | token_type => Type, 96 | expires_in => ExpiresIn}. 97 | 98 | -spec revoke_refresh_token(binary(), map(), map()) -> map(). 99 | revoke_refresh_token(Akey, Tokens, Rdesc) -> 100 | #{account := 101 | #{pool := Pool, 102 | bucket := Ab}} = Rdesc, 103 | #{iss := Iss, 104 | aud := Aud, 105 | refresh_token := 106 | #{alg := RefreshAlg}} = Tokens, 107 | 108 | KVpid0 = riakc_pool:lock(Pool), 109 | A = riakauth_account:get(KVpid0, Ab, Akey, [{pr, quorum}]), 110 | riakc_pool:unlock(Pool, KVpid0), 111 | 112 | %% We must not issue any tokens for disabled accounts. 113 | true = is_enabled(A), 114 | 115 | NowUs = idp:unix_time_us(), 116 | RefreshKey = generate_refresh_token_key(RefreshAlg), 117 | 118 | KVpid1 = riakc_pool:lock(Pool), 119 | _ = 120 | riakauth_account:put( 121 | KVpid1, Ab, Akey, 122 | riakauth_account:update_data_dt( 123 | fun(Data) -> 124 | update_refresh_token_dt(RefreshAlg, RefreshKey, NowUs, Data) 125 | end, A)), 126 | riakc_pool:unlock(Pool, KVpid1), 127 | 128 | RefreshToken = 129 | jose_jws_compact:encode( 130 | #{iss => Iss, 131 | aud => Aud, 132 | sub => Akey}, 133 | RefreshAlg, 134 | RefreshKey), 135 | 136 | #{refresh_token => RefreshToken}. 137 | 138 | -spec create(map(), map(), map(), map()) -> map(). 139 | create(ClientTokenPayload, Rdesc, Tokens, IdpsConf) -> 140 | #{key := [Prot, Prov]} = IdpsConf, 141 | #{account := 142 | #{pool := Pool, 143 | bucket := Ab, 144 | index := Index, 145 | handler := Hmod}} = Rdesc, 146 | #{type := Type, 147 | expires_in := ExpiresIn, 148 | iss := Iss, 149 | aud := Aud, 150 | access_token := 151 | #{alg := Alg, 152 | key := Key}, 153 | refresh_token := 154 | #{alg := RefreshAlg}} = Tokens, 155 | #{<<"sub">> := Uid} = ClientTokenPayload, 156 | Now = idp:unix_time(), 157 | NowUs = to_us(Now), 158 | Exp = Now +ExpiresIn, 159 | ExpUs = to_us(Exp), 160 | 161 | NewRefreshKey = generate_refresh_token_key(RefreshAlg), 162 | Identity = [Prot, Prov, Uid], 163 | HandleKey = fun idp:make_uuid/0, 164 | HandleData = 165 | fun(Data0) -> 166 | Data1 = update_refresh_token_dt(RefreshAlg, NewRefreshKey, NowUs, Data0), 167 | Data2 = update_enabled_dt(enable, Data1), 168 | Data2 169 | end, 170 | 171 | KVpid = riakc_pool:lock(Pool), 172 | {Akey, A} = riakauth:authenticate(KVpid, Ab, Index, Identity, HandleKey, HandleData), 173 | riakc_pool:unlock(Pool, KVpid), 174 | 175 | _ = 176 | case riakauth_account:find_data_rawdt(A) of 177 | {ok, Data} -> 178 | %% Already existed account. 179 | %% We must not issue any tokens or create ACL for disabled accounts. 180 | true = is_enabled_rawdt(Data); 181 | _ -> 182 | %% Newly created account. 183 | ignore 184 | end, 185 | 186 | Hmod:create_acl(ClientTokenPayload, Akey, NowUs, ExpUs, Rdesc, IdpsConf), 187 | 188 | AccessToken = 189 | jose_jws_compact:encode( 190 | #{iss => Iss, 191 | aud => Aud, 192 | sub => Akey, 193 | exp => Exp}, 194 | Alg, 195 | Key), 196 | 197 | RefreshKey = case refresh_token_dt(A) of #{key := Val} -> Val; _ -> NewRefreshKey end, 198 | RefreshToken = 199 | jose_jws_compact:encode( 200 | #{iss => Iss, 201 | aud => Aud, 202 | sub => Akey}, 203 | RefreshAlg, 204 | RefreshKey), 205 | 206 | %% According to RFC 6749 - The OAuth 2.0 Authorization Framework 207 | %% 5.1. Issuing an Access Token. Successful Response 208 | %% https://tools.ietf.org/html/rfc6749#section-5.1 209 | #{access_token => AccessToken, 210 | refresh_token => RefreshToken, 211 | token_type => Type, 212 | expires_in => ExpiresIn}. 213 | 214 | -spec link(map(), map(), map(), map(), map()) -> map(). 215 | link(ClientTokenPayload, AuthM, Rdesc, Tokens, IdpsConf) -> 216 | #{account := 217 | #{pool := Pool, 218 | bucket := Ab, 219 | handler := Hmod}} = Rdesc, 220 | #{key := [Prot, Prov]} = IdpsConf, 221 | #{expires_in := ExpiresIn} = Tokens, 222 | #{<<"sub">> := Uid} = ClientTokenPayload, 223 | #{<<"sub">> := Akey} = AuthM, 224 | 225 | Identity = [Prot, Prov, Uid], 226 | KVpid0 = riakc_pool:lock(Pool), 227 | A = riakauth_account:get(KVpid0, Ab, Akey), 228 | riakc_pool:unlock(Pool, KVpid0), 229 | 230 | %% We must not link new identities to disabled accounts. 231 | true = is_enabled(A), 232 | 233 | KVpid1 = riakc_pool:lock(Pool), 234 | _ = riakauth_account:put( 235 | KVpid1, Ab, Akey, 236 | riakauth_account:update_identity_dt(Identity, A)), 237 | riakc_pool:unlock(Pool, KVpid1), 238 | 239 | NowUs = idp:unix_time_us(), 240 | ExpUs = NowUs +to_us(ExpiresIn), 241 | Hmod:create_acl(ClientTokenPayload, Akey, NowUs, ExpUs, Rdesc, IdpsConf), 242 | 243 | #{id => <>}. 244 | 245 | -spec read(binary(), map()) -> {ok, riakauth_account:account()} | error. 246 | read(Akey, Rdesc) -> 247 | read(Akey, Rdesc, []). 248 | 249 | -spec read(binary(), map(), [proplists:property()]) -> {ok, riakauth_account:account()} | error. 250 | read(Akey, Rdesc, Opts) -> 251 | #{account := #{pool := KVpool, bucket := Ab}} = Rdesc, 252 | KVpid = riakc_pool:lock(KVpool), 253 | MaybeA = riakauth_account:find(KVpid, Ab, Akey, Opts), 254 | riakc_pool:unlock(KVpool, KVpid), 255 | MaybeA. 256 | 257 | -spec delete(binary(), riakauth_account:account(), map()) -> map(). 258 | delete(Akey, A, Rdesc) -> 259 | #{account := #{pool := KVpool, bucket := Ab}, 260 | account_aclsubject := #{pool := KVpool, bucket := AclSb}} = Rdesc, 261 | 262 | %% We do not allow deleting disabled accounts. 263 | true = is_enabled(A), 264 | 265 | KVpid = riakc_pool:lock(KVpool), 266 | riakacl_entry:remove(KVpid, AclSb, Akey), 267 | riakauth_account:remove(KVpid, Ab, Akey), 268 | riakc_pool:unlock(KVpool, KVpid), 269 | to_map(Akey, A). 270 | 271 | -spec enable(binary(), riakauth_account:account(), map()) -> ok. 272 | enable(Akey, A, Rdesc) -> 273 | enable_(enable, Akey, A, Rdesc). 274 | 275 | -spec disable(binary(), riakauth_account:account(), map()) -> ok. 276 | disable(Akey, A, Rdesc) -> 277 | enable_(disable, Akey, A, Rdesc). 278 | 279 | %% ============================================================================= 280 | %% API 281 | %% ============================================================================= 282 | 283 | -spec to_map(binary(), riakauth_account:account()) -> map(). 284 | to_map(Akey, A) -> 285 | format_resource(Akey, A). 286 | 287 | -spec is_enabled(riakauth_account:account()) -> boolean(). 288 | is_enabled(A) -> 289 | case riakauth_account:find_data_rawdt(A) of 290 | {ok, Data} -> is_enabled_rawdt(Data); 291 | _ -> false 292 | end. 293 | 294 | %% ============================================================================= 295 | %% DataType API 296 | %% ============================================================================= 297 | 298 | -spec is_enabled_rawdt([riakauth_account:rawdt()]) -> boolean(). 299 | is_enabled_rawdt(Data) -> 300 | case lists:keyfind({<<"enabled">>, flag}, 1, Data) of 301 | {_, Val} -> Val; 302 | _ -> false 303 | end. 304 | 305 | -spec update_enabled_dt(enable | disable, riakauth_account:data()) -> riakauth_account:account(). 306 | update_enabled_dt(Val, Data) -> 307 | riakc_map:update({<<"enabled">>, flag}, fun(Obj) -> riakc_flag:Val(Obj) end, Data). 308 | 309 | -spec generate_refresh_token_key(binary()) -> binary(). 310 | generate_refresh_token_key(Alg) -> 311 | jose_jwa:generate_key(Alg). 312 | 313 | -spec refresh_token_dt(riakauth:account()) -> map(). 314 | refresh_token_dt(A) -> 315 | fold_refresh_token_dt( 316 | fun 317 | ({{<<"alg">>, register}, Val}, Acc) -> Acc#{alg => Val}; 318 | ({{<<"key">>, register}, Val}, Acc) -> Acc#{key => cow_base64url:decode(Val)}; 319 | ({{<<"iat">>, register}, Val}, Acc) -> Acc#{iat => Val}; 320 | (_, Acc) -> Acc 321 | end, #{}, A). 322 | 323 | -spec update_refresh_token_dt(binary(), binary(), non_neg_integer(), riakauth_account:data()) -> riakauth_account:data(). 324 | update_refresh_token_dt(Alg, Key, IssuedAt, Data) -> 325 | riakc_map:update( 326 | {<<"refresh_token">>, map}, 327 | fun(T0) -> 328 | T1 = riakc_map:update({<<"alg">>, register}, fun(Obj) -> riakc_register:set(Alg, Obj) end, T0), 329 | T2 = riakc_map:update({<<"key">>, register}, fun(Obj) -> riakc_register:set(cow_base64url:encode(Key), Obj) end, T1), 330 | T3 = riakc_map:update({<<"iat">>, register}, fun(Obj) -> riakc_register:set(integer_to_binary(IssuedAt), Obj) end, T2), 331 | T3 332 | end, 333 | Data). 334 | 335 | -spec fold_refresh_token_dt(fun((riakauth_account:rawdt(), any()) -> any()), any(), riakauth:account()) -> any(). 336 | fold_refresh_token_dt(Handle, AccIn, A) -> 337 | case riakauth_account:find_data_rawdt(A) of 338 | {ok, Data} -> 339 | case lists:keyfind({<<"refresh_token">>, map}, 1, Data) of 340 | {_, Input} -> 341 | lists:foldl(Handle, AccIn, Input); 342 | _ -> 343 | %% There is no "refresh_token" property in the account object, 344 | %% so that our work is done. 345 | AccIn 346 | end; 347 | _ -> 348 | %% There is no "data" property in the account object, 349 | %% so that our work is done. 350 | AccIn 351 | end. 352 | 353 | %% ============================================================================= 354 | %% Internal functions 355 | %% ============================================================================= 356 | 357 | -spec enable_(enable | disable, binary(), riakauth_account:account(), map()) -> ok. 358 | enable_(Op, Akey, A, Rdesc) -> 359 | #{account := #{pool := KVpool, bucket := Ab}} = Rdesc, 360 | KVpid = riakc_pool:lock(KVpool), 361 | _ = 362 | riakauth_account:put( 363 | KVpid, Ab, Akey, 364 | riakauth_account:update_data_dt( 365 | fun(Data) -> 366 | idp_account:update_enabled_dt(Op, Data) 367 | end, A)), 368 | riakc_pool:unlock(KVpool, KVpid), 369 | ok. 370 | 371 | -spec format_resource(binary(), riakauth_account:account()) -> map(). 372 | format_resource(Tkey, _T) -> 373 | #{id => Tkey}. 374 | 375 | -spec to_us(non_neg_integer()) -> non_neg_integer(). 376 | to_us(Sec) -> 377 | Sec *1000000. 378 | -------------------------------------------------------------------------------- /src/idp_account_auth.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_account_auth). 26 | 27 | %% CRUD API 28 | -export([ 29 | list/2, 30 | read/3, 31 | delete/4 32 | ]). 33 | 34 | %% API 35 | -export([ 36 | identity/1, 37 | parse_identity/1 38 | ]). 39 | 40 | %% Types 41 | -type resource() :: [riakacl_group:rawdt()]. 42 | 43 | -record(rbox, { 44 | r :: resource(), 45 | p :: riakauth_account:account() 46 | }). 47 | -type rbox() :: #rbox{}. 48 | 49 | %% ============================================================================= 50 | %% CRUD API 51 | %% ============================================================================= 52 | 53 | -spec list([binary()], riakauth_account:account()) -> [map()]. 54 | list(AuthKeys, A) -> 55 | format_resources(AuthKeys, A). 56 | 57 | -spec read(binary(), binary(), map()) -> {ok, rbox()} | error. 58 | read(Akey, Ibin, Rdesc) -> 59 | #{account := #{pool := KVpool, bucket := Ab}} = Rdesc, 60 | KVpid = riakc_pool:lock(KVpool), 61 | MaybeA = riakauth_account:find(KVpid, Ab, Akey), 62 | riakc_pool:unlock(KVpool, KVpid), 63 | case MaybeA of 64 | {ok, A} -> find_resource(parse_identity(Ibin), A); 65 | _ -> error 66 | end. 67 | 68 | -spec delete(binary(), binary(), rbox(), map()) -> map(). 69 | delete(Akey, Ibin, #rbox{p = A0} =Rbox, Rdesc) -> 70 | #{account := #{pool := KVpool, bucket := Ab}} = Rdesc, 71 | 72 | %% We do not allow deleting identities of disabled accounts. 73 | true = idp_account:is_enabled(A0), 74 | 75 | A1 = riakauth_account:remove_identity_dt(parse_identity(Ibin), A0), 76 | KVpid = riakc_pool:lock(KVpool), 77 | _ = riakauth_account:put(KVpid, Ab, Akey, A1), 78 | riakc_pool:unlock(KVpool, KVpid), 79 | to_map(Ibin, Rbox). 80 | 81 | %% ============================================================================= 82 | %% API 83 | %% ============================================================================= 84 | 85 | -spec to_map(binary(), rbox()) -> map(). 86 | to_map(Ibin, #rbox{r = Raw}) -> 87 | format_resource_dt(Ibin, Raw). 88 | 89 | -spec identity(riakauth_account:identity()) -> binary(). 90 | identity([]) -> <<>>; 91 | identity([H|T]) -> identity(T, H). 92 | 93 | -spec parse_identity(binary()) -> [binary()]. 94 | parse_identity(Bin) -> 95 | parse_identity(Bin, <<>>, []). 96 | 97 | %% ============================================================================= 98 | %% Internal functions 99 | %% ============================================================================= 100 | 101 | -spec identity(riakauth_account:identity(), binary()) -> binary(). 102 | identity([Val|T], Acc) -> identity(T, <>); 103 | identity([], Acc) -> Acc. 104 | 105 | -spec parse_identity(binary(), binary(), [binary()]) -> [binary()]. 106 | parse_identity(<<$., Rest/bits>>, AccV, AccL) -> parse_identity(Rest, <<>>, [AccV|AccL]); 107 | parse_identity(<>, AccV, AccL) -> parse_identity(Rest, <>, AccL); 108 | parse_identity(<<>>, AccV, AccL) -> lists:reverse([AccV|AccL]). 109 | 110 | -spec format_resources([binary()], riakauth_account:account()) -> [map()]. 111 | format_resources(AuthKeys, A) -> 112 | riakauth_account:fold_identities_dt( 113 | fun(Identity, Raw, Acc) -> 114 | [format_resource_dt(identity(Identity), Raw) | Acc] 115 | end, AuthKeys, [], A). 116 | 117 | -spec format_resource_dt(binary(), resource()) -> map(). 118 | format_resource_dt(Ibin, _Raw) -> 119 | #{id => Ibin}. 120 | 121 | -spec find_resource(riakauth_account:identity(), riakauth_account:account()) -> {ok, rbox()} | error. 122 | find_resource(Identity, A) -> 123 | case riakauth_account:find_identity_rawdt(Identity, A) of 124 | {ok, Raw} -> {ok, #rbox{r = Raw, p = A}}; 125 | error -> error 126 | end. 127 | -------------------------------------------------------------------------------- /src/idp_accounth.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_accounth). 26 | 27 | %% Callbacks 28 | -callback create_acl(ClientTokenPayload, AccountKey, CreatedAtUs, ExpiredAtUs, Resources, IdentityProviders) -> Result 29 | when 30 | ClientTokenPayload :: map(), 31 | AccountKey :: binary(), 32 | CreatedAtUs :: non_neg_integer(), 33 | ExpiredAtUs :: non_neg_integer(), 34 | Resources :: map(), 35 | IdentityProviders :: map(), 36 | Result :: any(). 37 | -------------------------------------------------------------------------------- /src/idp_accounth_stub.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_accounth_stub). 26 | -behaviour(idp_accounth). 27 | 28 | %% Account handler callbacks 29 | -export([ 30 | create_acl/6 31 | ]). 32 | 33 | %% ---------------------------------------------------------------------------- 34 | %% Account handler callbacks 35 | %% ---------------------------------------------------------------------------- 36 | 37 | -spec create_acl(map(), binary(), non_neg_integer(), non_neg_integer(), map(), map()) -> any(). 38 | create_acl(_ClientTokenPayload, _AccountKey, _CreatedAtUs, _ExpiredAtUs, _Resources, _IdentityProviders) -> 39 | ignore. 40 | -------------------------------------------------------------------------------- /src/idp_app.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_app). 26 | -behaviour(application). 27 | 28 | %% Application callbacks 29 | -export([ 30 | start/2, 31 | stop/1 32 | ]). 33 | 34 | %% ============================================================================= 35 | %% Application callbacks 36 | %% ============================================================================= 37 | 38 | start(_Type, _Args) -> 39 | {ok, _} = idp_http:start(), 40 | idp_sup:start_link(). 41 | 42 | stop(_State) -> 43 | ok. 44 | -------------------------------------------------------------------------------- /src/idp_auth.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_auth). 26 | 27 | %% Types 28 | -type key() :: riakauth_account:identity(). 29 | -type identity() :: riakauth_account:identity(). 30 | 31 | -export_type([key/0, identity/0]). 32 | -------------------------------------------------------------------------------- /src/idp_cli_account.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_cli_account). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% API 30 | -export([ 31 | create/2, 32 | create/3, 33 | remove/1 34 | ]). 35 | 36 | %% Types 37 | -type create_options() :: 38 | #{acl => [{binary(), riakacl_group:group()}], 39 | identities => [[binary()]], 40 | enable => boolean(), 41 | id => binary()}. 42 | 43 | %% ============================================================================= 44 | %% API 45 | %% ============================================================================= 46 | 47 | -spec create(create_options(), map()) -> map(). 48 | create(Opts, TokenOpts) -> 49 | #{account := #{pool := KVpool}, account_aclsubject := #{pool := KVpool}} = idp:resources(), 50 | KVpid = riakc_pool:lock(KVpool), 51 | Result = create(KVpid, Opts, TokenOpts), 52 | riakc_pool:unlock(KVpool, KVpid), 53 | Result. 54 | 55 | -spec create(pid(), create_options(), map()) -> map(). 56 | create(KVpid, Opts, TokenOpts) -> 57 | #{account := #{bucket := Ab}, 58 | account_aclsubject := #{bucket := AclSb}} = idp:resources(), 59 | #{expires_in := ExpiresIn, 60 | iss := Iss, 61 | aud := Aud, 62 | access_token := #{keyfile := KeyFile}, 63 | refresh_token := #{alg := RefreshAlg}} = maps:merge(idp:tokens(), TokenOpts), 64 | 65 | {ok, Pem} = file:read_file(KeyFile), 66 | {Alg, Priv} = jose_pem:parse_key(Pem), 67 | Akey = 68 | case maps:find(id, Opts) of 69 | {ok, Val} -> Val; 70 | _ -> idp:make_uuid() 71 | end, 72 | Tpayload0 = 73 | #{iss => Iss, 74 | aud => Aud, 75 | sub => Akey}, 76 | Tpayload1 = 77 | case ExpiresIn of 78 | infinity -> Tpayload0; 79 | _ -> Tpayload0#{exp => idp:unix_time() +ExpiresIn} 80 | end, 81 | AccessToken = jose_jws_compact:encode(Tpayload1, Alg, Priv), 82 | 83 | NowUs = idp:unix_time_us(), 84 | RefreshKey = idp_account:generate_refresh_token_key(RefreshAlg), 85 | HandleData = 86 | fun(Data0) -> 87 | Data1 = idp_account:update_refresh_token_dt(RefreshAlg, RefreshKey, NowUs, Data0), 88 | Data2 = 89 | case maps:get(enable, Opts, true) of 90 | true -> idp_account:update_enabled_dt(enable, Data1); 91 | _ -> Data1 92 | end, 93 | Data2 94 | end, 95 | RefreshToken = 96 | jose_jws_compact:encode( 97 | #{iss => Iss, 98 | aud => Aud, 99 | sub => Akey}, 100 | RefreshAlg, 101 | RefreshKey), 102 | 103 | AclGroups = maps:get(acl, Opts, []), 104 | A0 = riakauth_account:update_data_dt(HandleData, riakauth_account:new_dt()), 105 | A1 = 106 | case maps:find(identities, Opts) of 107 | {ok, L} -> lists:foldl(fun(I, Acc) -> riakauth_account:update_identity_dt(I, Acc) end, A0, L); 108 | _ -> A0 109 | end, 110 | 111 | _ = riakauth_account:put(KVpid, Ab, Akey, A1), 112 | _ = riakacl:put_subject_groups(KVpid, AclSb, Akey, AclGroups), 113 | ?INFO_REPORT( 114 | [ {reason, access_token_issued}, 115 | {access_token, Tpayload1}, 116 | {aclgroups, [Gname || {Gname, _} <- AclGroups]} ]), 117 | 118 | #{id => Akey, access_token => AccessToken, refresh_token => RefreshToken}. 119 | 120 | -spec remove(binary()) -> ok. 121 | remove(Akey) -> 122 | #{account := #{pool := KVpool, bucket := Ab}, 123 | account_aclsubject := #{pool := KVpool, bucket := AclSb}} = idp:resources(), 124 | KVpid = riakc_pool:lock(KVpool), 125 | riakacl_entry:remove(KVpid, AclSb, Akey), 126 | riakauth_account:remove(KVpid, Ab, Akey), 127 | riakc_pool:unlock(KVpool, KVpid), 128 | ok. 129 | -------------------------------------------------------------------------------- /src/idp_constraint.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_constraint). 26 | 27 | %% API 28 | -export([ 29 | binary/2, 30 | int/2 31 | ]). 32 | 33 | %% Types 34 | -type constraint() :: fun((forward | reverse | format_error, iodata()) -> {ok, any()} | {error, any()} | iodata()). 35 | 36 | %% ============================================================================= 37 | %% API 38 | %% ============================================================================= 39 | 40 | -spec binary(non_neg_integer(), non_neg_integer()) -> constraint(). 41 | binary(Min, Max) -> 42 | fun 43 | (forward, Val) -> 44 | try 45 | Size = iolist_size(Val), 46 | true = Size =< Max, 47 | true = Size >= Min, 48 | {ok, Val} 49 | catch _:_ -> {error, {invalid_binary, Val}} end; 50 | (reverse, Val) -> 51 | {ok, Val}; 52 | (format_error, {invalid_binary, Val}) -> 53 | <<"The value ", Val/binary, 54 | "should be a binary whose length is (", (integer_to_binary(Min))/binary, 55 | $,, (integer_to_binary(Max))/binary, ").">> 56 | end. 57 | 58 | -spec int(non_neg_integer(), non_neg_integer()) -> constraint(). 59 | int(Min, Max) -> 60 | fun 61 | (forward, Val) -> 62 | try 63 | Num = binary_to_integer(Val), 64 | true = Num =< Max, 65 | true = Num >= Min, 66 | {ok, Num} 67 | catch _:_ -> {error, {invalid_integer, Val}} end; 68 | (reverse, Val) -> 69 | try {ok, integer_to_binary(Val)} 70 | catch _:_ -> {error, {invalid_integer, Val}} end; 71 | (format_error, {invalid_integer, Val}) -> 72 | <<"The value ", (integer_to_binary(Val))/binary, 73 | "should be an integer whose range is (", (integer_to_binary(Min))/binary, 74 | $,, (integer_to_binary(Max))/binary, ").">> 75 | end. 76 | -------------------------------------------------------------------------------- /src/idp_http.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_http). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% API 30 | -export([ 31 | start/0, 32 | access_token_type/0, 33 | access_token/1, 34 | find_access_token/1, 35 | decode_access_token/2, 36 | decode_refresh_token/3, 37 | handle_response/3, 38 | handle_response/4, 39 | control_response/5, 40 | handle_payload/3, 41 | handle_payload/4, 42 | handle_payload/5, 43 | control_payload/5, 44 | encode_payload/2 45 | ]). 46 | 47 | %% Definitions 48 | -define(DEFAULT_CONTENT_TYPE, <<"application/json">>). 49 | -define(AUTHORIZATION, <<"authorization">>). 50 | -define(BEARER, <<"Bearer">>). 51 | 52 | %% Types 53 | 54 | %% TODO: probably it shoud be just an opaque binary, 55 | %% because we can't control all possible error reasons, 56 | %% but we can convert them to a binary instead. 57 | %% On the other hand, there is could be a problem with binary 58 | %% (which is encoded json for instance) - creating nesting data structures. 59 | -type payload() :: map() | binary(). 60 | 61 | %% ============================================================================= 62 | %% API 63 | %% ============================================================================= 64 | 65 | start() -> 66 | Env = 67 | #{dispatch => dispatch(), 68 | allowed_origins => idp:allowed_origins(), 69 | preflight_request_max_age => idp:preflight_request_max_age()}, 70 | HttpOpts = idp:http_options(), 71 | HttpdRequiredOpts = 72 | #{stream_handlers => [idp_streamh_log, cowboy_stream_h], 73 | middlewares => [idp_httpm_cors, cowboy_router, cowboy_handler], 74 | env => Env}, 75 | HttpdStart = 76 | case lists:keyfind(certfile, 1, HttpOpts) of 77 | {certfile, _Val} -> start_tls; 78 | _ -> start_clear 79 | end, 80 | cowboy:HttpdStart( 81 | httpd, 82 | HttpOpts, 83 | maps:merge(HttpdRequiredOpts, idp:httpd_options())). 84 | 85 | -spec dispatch() -> cowboy_router:dispatch_rules(). 86 | dispatch() -> 87 | cowboy_router:compile(routes()). 88 | 89 | -spec access_token_type() -> binary(). 90 | access_token_type() -> ?BEARER. 91 | 92 | -spec find_access_token(cowboy_req:req()) -> {ok, binary()} | error. 93 | find_access_token(Req) -> 94 | case cowboy_req:parse_header(?AUTHORIZATION, Req) of 95 | {bearer, Token} -> {ok, Token}; 96 | _ -> error 97 | end. 98 | 99 | -spec access_token(cowboy_req:req()) -> binary(). 100 | access_token(Req) -> 101 | case find_access_token(Req) of 102 | {ok, Token} -> Token; 103 | _ -> throw(missing_access_token) 104 | end. 105 | 106 | -spec decode_access_token(cowboy_req:req(), map()) -> map(). 107 | decode_access_token(Req, AuthConf) -> 108 | case find_access_token(Req) of 109 | {ok, Token} -> idp:decode_access_token(Token, AuthConf); 110 | _ -> #{} 111 | end. 112 | 113 | -spec decode_refresh_token(cowboy_req:req(), map(), map()) -> map(). 114 | decode_refresh_token(Req, AuthConf, Rdesc) -> 115 | case find_access_token(Req) of 116 | {ok, Token} -> idp:decode_refresh_token(Token, AuthConf, Rdesc); 117 | _ -> #{} 118 | end. 119 | 120 | -spec handle_response(Req, State, HandleSuccess) -> {Result, Req, State} 121 | when 122 | Req :: cowboy_req:req(), 123 | State :: any(), 124 | HandleSuccess :: fun(() -> ok | binary() | map()), 125 | Result :: true | stop | binary(). 126 | handle_response(Req, State, Handler) -> 127 | handle_response(Req, State, ?DEFAULT_CONTENT_TYPE, Handler). 128 | 129 | -spec handle_response(Req, State, ContentType, HandleSuccess) -> {Result, Req, State} 130 | when 131 | Req :: cowboy_req:req(), 132 | State :: any(), 133 | ContentType :: binary(), 134 | HandleSuccess :: fun(() -> ok | binary() | map()), 135 | Result :: true | stop | binary(). 136 | handle_response(Req, State, ContentType, HandleSuccess) -> 137 | HandleFailure = 138 | fun(_Fpayload, Freq, Fstate) -> 139 | {stop, cowboy_req:reply(422, Freq), Fstate} 140 | end, 141 | control_response(Req, State, ContentType, HandleSuccess, HandleFailure). 142 | 143 | -spec control_response(Req, State, ContentType, HandleSuccess, HandleFailure) -> {Result, Req, State} 144 | when 145 | Req :: cowboy_req:req(), 146 | State :: any(), 147 | ContentType :: binary(), 148 | HandleSuccess :: fun(() -> ok | binary() | map()), 149 | HandleFailure :: fun((payload(), Req, any()) -> {Result, Req, State}), 150 | Result :: true | stop | binary(). 151 | control_response(Req, State, ContentType, HandleSuccess, HandleFailure) -> 152 | AfterSuccess = 153 | fun 154 | (<<"GET">>, Body) -> {encode_payload(ContentType, Body), Req, State}; 155 | (<<"HEAD">>, Body) -> {encode_payload(ContentType, Body), Req, State}; 156 | (_Method, ok) -> {true, Req, State}; 157 | (_Method, Body) -> 158 | Req2 = cowboy_req:set_resp_header(<<"content-type">>, ContentType, Req), 159 | Req3 = cowboy_req:set_resp_body(encode_payload(ContentType, Body), Req2), 160 | {true, Req3, State} 161 | end, 162 | try HandleSuccess() of 163 | Body -> AfterSuccess(maps:get(method, Req), Body) 164 | catch 165 | T:R -> 166 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 167 | %% TODO: provide an informative error payload as a first argument 168 | HandleFailure(#{}, Req, State) 169 | end. 170 | 171 | -spec handle_payload(Req, State, HandleSuccess) -> {Result, Req, State} 172 | when 173 | Req :: cowboy_req:req(), 174 | State :: any(), 175 | HandleSuccess :: fun((any(), Req) -> {Result, Req, State}), 176 | Result :: true | stop | binary(). 177 | handle_payload(Req, State, HandleSuccess) -> 178 | handle_payload(Req, State, #{}, HandleSuccess). 179 | 180 | -spec handle_payload(Req, State, ReadOpts, HandleSuccess) -> {Result, Req, State} 181 | when 182 | Req :: cowboy_req:req(), 183 | State :: any(), 184 | ReadOpts :: map(), 185 | HandleSuccess :: fun((any(), Req) -> {Result, Req, State}), 186 | Result :: true | stop | binary(). 187 | handle_payload(Req, State, ReadOpts, HandleSuccess) -> 188 | handle_payload(Req, State, ReadOpts, ?DEFAULT_CONTENT_TYPE, HandleSuccess). 189 | 190 | -spec handle_payload(Req, State, ReadOpts, RespContentType, HandleSuccess) -> {Result, Req, State} 191 | when 192 | Req :: cowboy_req:req(), 193 | State :: any(), 194 | ReadOpts :: map(), 195 | RespContentType :: binary(), 196 | HandleSuccess :: fun((any(), Req) -> {Result, Req, State}), 197 | Result :: true | stop | binary(). 198 | handle_payload(Req, State, ReadOpts, RespContentType, HandleSuccess) -> 199 | HandleFailure = 200 | fun 201 | %% Only POST, PUT, PATCH requests have a payload 202 | (Fpayload, Freq0, Fstate) -> 203 | Freq1 = cowboy_req:set_resp_header(<<"content-type">>, RespContentType, Freq0), 204 | Freq2 = cowboy_req:set_resp_body(encode_payload(RespContentType, Fpayload), Freq1), 205 | {false, Freq2, Fstate} 206 | end, 207 | control_payload(Req, State, ReadOpts, HandleSuccess, HandleFailure). 208 | 209 | -spec control_payload(Req, State, ReadOpts, HandleSuccess, HandleFailure) -> {Result, Req, State} 210 | when 211 | Req :: cowboy_req:req(), 212 | State :: any(), 213 | ReadOpts :: map(), 214 | HandleSuccess :: fun((any(), Req) -> {Result, Req, State}), 215 | HandleFailure :: fun((payload(), Req, any()) -> {Result, Req, State}), 216 | Result :: true | stop | binary(). 217 | control_payload(Req0, State, ReadOpts, HandleSuccess, HandleFailure) -> 218 | case cowboy_req:read_body(Req0, ReadOpts) of 219 | {ok, <<>>, Req1} -> 220 | HandleFailure(#{error => missing_payload}, Req1, State); 221 | {ok, Body, Req1} -> 222 | try HandleSuccess(Body, Req1) 223 | catch T:R -> 224 | ?ERROR_REPORT(idp_http_log:format_request(Req1), T, R), 225 | HandleFailure(#{error => bad_payload, payload => Body}, Req1, State) 226 | end; 227 | {more, _, Req1} -> 228 | HandleFailure(#{error => bad_payload_length}, Req1, State) 229 | end. 230 | 231 | -spec encode_payload(binary(), payload()) -> iodata(). 232 | encode_payload(_ContentType, Body) when is_binary(Body) -> Body; 233 | encode_payload(<<"application/json", _/bits>>, Body) -> jsx:encode(Body); 234 | encode_payload(ContentType, _Body) -> error({unsupported_content_type, ContentType}). 235 | 236 | %% ============================================================================= 237 | %% Internal functions 238 | %% ============================================================================= 239 | 240 | -spec routes() -> list(tuple()). 241 | routes() -> 242 | Auth = 243 | lists:foldl( 244 | fun 245 | (#{key := [<<"oauth2">> | _] = Key, options := Opts} = Conf0, Acc) -> 246 | Conf1 = 247 | try 248 | #{resources => idp:resources(), 249 | tokens => idp:tokens(), 250 | authentication => idp:authentication(), 251 | identity_providers => Conf0#{options => load_auth_key(Opts)}} 252 | catch throw:Reason -> error({bad_auth_config, Reason, Conf0}) end, 253 | [ authtoken_route(Key, idp_httph_oauth2_token, Conf1), 254 | authlink_route(Key, idp_httph_oauth2_token, Conf1) 255 | | Acc ]; 256 | (Conf, _Acc) -> 257 | error({bad_auth_config, bad_prot, Conf}) 258 | end, [], idp:identity_providers()), 259 | 260 | Opts = #{resources => idp:resources(), authentication => idp:authentication()}, 261 | AuthKeys = [AuthKey || #{key := AuthKey} <- idp:identity_providers()], 262 | Accounts = 263 | [ {<<"/api[/v1]/accounts/:key/refresh">>, idp_httph_account_refresh, Opts#{tokens => idp:tokens()}}, 264 | {<<"/api[/v1]/accounts/:key/revoke">>, idp_httph_account_revoke, Opts#{tokens => idp:tokens()}}, 265 | {<<"/api[/v1]/accounts/:key/enabled">>, idp_httph_account_enabled, Opts}, 266 | {<<"/api[/v1]/accounts/:key/auth/:identity">>, idp_httph_account_auth, Opts}, 267 | {<<"/api[/v1]/accounts/:key/auth">>, idp_httph_account_auths, Opts#{authentication_keys => AuthKeys}}, 268 | {<<"/api[/v1]/accounts/:key">>, idp_httph_account, Opts} ], 269 | 270 | [{'_', Auth ++ Accounts}]. 271 | 272 | -spec load_auth_key(map()) -> map(). 273 | load_auth_key(#{alg := _, key := _} =M) -> M; 274 | load_auth_key(#{keyfile := Path} =M) -> 275 | try 276 | {ok, Pem} = file:read_file(idp:conf_path(Path)), 277 | {Alg, Key} = jose_pem:parse_key(Pem), 278 | M#{alg => Alg, key => Key} 279 | catch _:_ -> 280 | throw({bad_keyfile, Path}) 281 | end; 282 | load_auth_key(_) -> 283 | throw(missing_key). 284 | 285 | -spec authtoken_route(idp_auth:key(), module(), map()) -> tuple(). 286 | authtoken_route([Prot, Prov], Handler, Opts) -> 287 | KeyB = <>, 288 | Uri = <<"/api[/v1]/auth/", KeyB/binary, "/token">>, 289 | {Uri, Handler, Opts#{operation => create}}. 290 | 291 | -spec authlink_route(idp_auth:key(), module(), map()) -> tuple(). 292 | authlink_route([Prot, Prov], Handler, Opts) -> 293 | KeyB = <>, 294 | Uri = <<"/api[/v1]/auth/", KeyB/binary, "/link">>, 295 | {Uri, Handler, Opts#{operation => link}}. 296 | -------------------------------------------------------------------------------- /src/idp_http_log.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_http_log). 26 | 27 | %% API 28 | -export([ 29 | format_request/1, 30 | format_unauthenticated_request/1, 31 | format_response/2, 32 | format_response/3 33 | ]). 34 | 35 | %% Types 36 | -type kvlist() :: [{atom(), any()}]. 37 | 38 | %% ============================================================================= 39 | %% API 40 | %% ============================================================================= 41 | 42 | -spec format_request(cowboy_req:req()) -> kvlist(). 43 | format_request(Req) -> 44 | #{pid := Pid, 45 | streamid := StreamId, 46 | method := Method, 47 | version := Version, 48 | headers := Headers, 49 | peer := {Addr, Port}} = Req, 50 | Acc = 51 | [ {http_pid, Pid}, 52 | {http_streamid, StreamId}, 53 | {http_uri, iolist_to_binary(cowboy_req:uri(Req))}, 54 | {http_method, Method}, 55 | {http_version, Version}, 56 | {http_peer, <<(list_to_binary(inet:ntoa(Addr)))/binary, $:, (integer_to_binary(Port))/binary>>} ], 57 | add_optional_map_property(http_referer, <<"referer">>, Headers, 58 | add_optional_map_property(http_user_agent, <<"user-agent">>, Headers, Acc)). 59 | 60 | -spec format_unauthenticated_request(cowboy_req:req()) -> kvlist(). 61 | format_unauthenticated_request(#{headers := Headers} =Req) -> 62 | add_optional_map_property(http_authorization_header, <<"authorization">>, Headers, format_request(Req)). 63 | 64 | -spec format_response(integer(), map()) -> kvlist(). 65 | format_response(Status, Headers) -> 66 | format_response(Status, Headers, []). 67 | 68 | -spec format_response(integer(), map(), kvlist()) -> kvlist(). 69 | format_response(Status, Headers, Acc) -> 70 | [ {http_response_headers, Headers}, 71 | {http_status_code, Status} 72 | | Acc]. 73 | 74 | %% ============================================================================= 75 | %% Internal function 76 | %% ============================================================================= 77 | 78 | -spec add_optional_map_property(atom(), any(), map(), kvlist()) -> kvlist(). 79 | add_optional_map_property(Label, Key, M, Acc) -> 80 | case maps:find(Key, M) of 81 | {ok, Val} -> [{Label, Val}|Acc]; 82 | _ -> Acc 83 | end. 84 | -------------------------------------------------------------------------------- /src/idp_httph_account.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httph_account). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% REST handler callbacks 30 | -export([ 31 | init/2, 32 | is_authorized/2, 33 | forbidden/2, 34 | resource_exists/2, 35 | delete_resource/2, 36 | content_types_provided/2, 37 | allowed_methods/2, 38 | options/2 39 | ]). 40 | 41 | %% Content callbacks 42 | -export([ 43 | to_json/2 44 | ]). 45 | 46 | %% Types 47 | -record(state, { 48 | rdesc :: map(), 49 | authconf :: map(), 50 | key = undefined :: undefined | iodata(), 51 | authm = #{} :: map(), 52 | r = undefined :: undefined | riakauth_account:account() 53 | }). 54 | 55 | %% ============================================================================= 56 | %% REST handler callbacks 57 | %% ============================================================================= 58 | 59 | init(Req, Opts) -> 60 | #{authentication := AuthConf, resources := Rdesc} = Opts, 61 | State = 62 | #state{ 63 | rdesc = Rdesc, 64 | authconf = AuthConf, 65 | key = cowboy_req:binding(key, Req)}, 66 | {cowboy_rest, Req, State}. 67 | 68 | is_authorized(#{method := <<"OPTIONS">>} =Req, State) -> {true, Req, State}; 69 | is_authorized(Req, #state{authconf = AuthConf} =State) -> 70 | try idp_http:decode_access_token(Req, AuthConf) of 71 | TokenPayload -> 72 | ?INFO_REPORT([{access_token, TokenPayload} | idp_http_log:format_request(Req)]), 73 | {true, Req, State#state{authm = TokenPayload}} 74 | catch 75 | T:R -> 76 | ?ERROR_REPORT(idp_http_log:format_unauthenticated_request(Req), T, R), 77 | {{false, idp_http:access_token_type()}, Req, State} 78 | end. 79 | 80 | forbidden(#{method := <<"OPTIONS">>} =Req, State) -> {false, Req, State}; 81 | forbidden(Req, #state{key = Key, authm = AuthM, rdesc = Rdesc} =State) -> 82 | try idp:authorize_subject(Key, AuthM, Rdesc) of 83 | {ok, Skey, #{write := true}} -> {false, Req, State#state{key = Skey}}; 84 | _ -> {true, Req, State} 85 | catch T:R -> 86 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 87 | {stop, cowboy_req:reply(422, Req), State} 88 | end. 89 | 90 | resource_exists(Req, #state{key = Key, rdesc = Rdesc} =State) -> 91 | try idp_account:read(Key, Rdesc) of 92 | {ok, R} -> {true, Req, State#state{r = R}}; 93 | _ -> {false, Req, State} 94 | catch T:R -> 95 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 96 | {stop, cowboy_req:reply(422, Req), State} 97 | end. 98 | 99 | delete_resource(Req, #state{key = Akey, r = A, rdesc = Rdesc} =State) -> 100 | idp_http:handle_response(Req, State, fun() -> 101 | jsx:encode(idp_account:delete(Akey, A, Rdesc)) 102 | end). 103 | 104 | content_types_provided(Req, State) -> 105 | Handlers = [{{<<"application">>, <<"json">>, '*'}, to_json}], 106 | {Handlers, Req, State}. 107 | 108 | allowed_methods(Req, State) -> 109 | Methods = [<<"GET">>, <<"DELETE">>, <<"OPTIONS">>], 110 | {Methods, Req, State}. 111 | 112 | options(Req0, State) -> 113 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"GET, DELETE">>, Req0), 114 | Req2 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"authorization">>, Req1), 115 | Req3 = cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, <<"true">>, Req2), 116 | {ok, Req3, State}. 117 | 118 | %% ============================================================================= 119 | %% Content callbacks 120 | %% ============================================================================= 121 | 122 | to_json(Req, #state{key = Akey, r = A} =State) -> 123 | idp_http:handle_response(Req, State, fun() -> 124 | jsx:encode(idp_account:to_map(Akey, A)) 125 | end). 126 | -------------------------------------------------------------------------------- /src/idp_httph_account_auth.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httph_account_auth). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% REST handler callbacks 30 | -export([ 31 | init/2, 32 | is_authorized/2, 33 | forbidden/2, 34 | resource_exists/2, 35 | delete_resource/2, 36 | content_types_provided/2, 37 | allowed_methods/2, 38 | options/2 39 | ]). 40 | 41 | %% Types 42 | -record(state, { 43 | rdesc :: map(), 44 | authconf :: map(), 45 | key = undefined :: undefined | iodata(), 46 | identity = undefined :: undefined | iodata(), 47 | authm = #{} :: map(), 48 | rbox = undefined :: undefined | idp_account_auth:rbox() 49 | }). 50 | 51 | %% ============================================================================= 52 | %% REST handler callbacks 53 | %% ============================================================================= 54 | 55 | init(Req, Opts) -> 56 | #{authentication := AuthConf, resources := Rdesc} = Opts, 57 | State = 58 | #state{ 59 | rdesc = Rdesc, 60 | authconf = AuthConf, 61 | key = cowboy_req:binding(key, Req), 62 | identity = cowboy_req:binding(identity, Req)}, 63 | {cowboy_rest, Req, State}. 64 | 65 | is_authorized(#{method := <<"OPTIONS">>} =Req, State) -> {true, Req, State}; 66 | is_authorized(Req, #state{authconf = AuthConf} =State) -> 67 | try idp_http:decode_access_token(Req, AuthConf) of 68 | TokenPayload -> 69 | ?INFO_REPORT([{access_token, TokenPayload} | idp_http_log:format_request(Req)]), 70 | {true, Req, State#state{authm = TokenPayload}} 71 | catch 72 | T:R -> 73 | ?ERROR_REPORT(idp_http_log:format_unauthenticated_request(Req), T, R), 74 | {{false, idp_http:access_token_type()}, Req, State} 75 | end. 76 | 77 | forbidden(#{method := <<"OPTIONS">>} =Req, State) -> {false, Req, State}; 78 | forbidden(Req, #state{key = Key, authm = AuthM, rdesc = Rdesc} =State) -> 79 | try idp:authorize_subject(Key, AuthM, Rdesc) of 80 | {ok, Skey, #{write := true}} -> {false, Req, State#state{key = Skey}}; 81 | _ -> {true, Req, State} 82 | catch T:R -> 83 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 84 | {stop, cowboy_req:reply(422, Req), State} 85 | end. 86 | 87 | resource_exists(Req, #state{key = Key, identity = Ibin, rdesc = Rdesc} =State) -> 88 | try idp_account_auth:read(Key, Ibin, Rdesc) of 89 | {ok, Rbox} -> {true, Req, State#state{rbox = Rbox}}; 90 | _ -> {false, Req, State} 91 | catch T:R -> 92 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 93 | {stop, cowboy_req:reply(422, Req), State} 94 | end. 95 | 96 | delete_resource(Req, #state{key = Key, identity = Ibin, rbox = Rbox, rdesc = Rdesc} =State) -> 97 | idp_http:handle_response(Req, State, fun() -> 98 | jsx:encode(idp_account_auth:delete(Key, Ibin, Rbox, Rdesc)) 99 | end). 100 | 101 | content_types_provided(Req, State) -> 102 | Handlers = [{{<<"application">>, <<"json">>, '*'}, ignore}], 103 | {Handlers, Req, State}. 104 | 105 | allowed_methods(Req, State) -> 106 | Methods = [<<"DELETE">>, <<"OPTIONS">>], 107 | {Methods, Req, State}. 108 | 109 | options(Req0, State) -> 110 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"DELETE">>, Req0), 111 | Req2 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"authorization">>, Req1), 112 | Req3 = cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, <<"true">>, Req2), 113 | {ok, Req3, State}. 114 | -------------------------------------------------------------------------------- /src/idp_httph_account_auths.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httph_account_auths). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% REST handler callbacks 30 | -export([ 31 | init/2, 32 | is_authorized/2, 33 | forbidden/2, 34 | resource_exists/2, 35 | content_types_provided/2, 36 | allowed_methods/2, 37 | options/2 38 | ]). 39 | 40 | %% Content callbacks 41 | -export([ 42 | to_json/2 43 | ]). 44 | 45 | %% Types 46 | -record(state, { 47 | rdesc :: map(), 48 | authconf :: map(), 49 | authkeys = [] :: [[binary()]], 50 | key = undefined :: undefined | iodata(), 51 | authm = #{} :: map(), 52 | p :: undefined | riakauth_account:account() 53 | }). 54 | 55 | %% ============================================================================= 56 | %% REST handler callbacks 57 | %% ============================================================================= 58 | 59 | init(Req, Opts) -> 60 | #{authentication_keys := AuthKeys, authentication := AuthConf, resources := Rdesc} = Opts, 61 | State = 62 | #state{ 63 | rdesc = Rdesc, 64 | authconf = AuthConf, 65 | authkeys = AuthKeys, 66 | key = cowboy_req:binding(key, Req)}, 67 | {cowboy_rest, Req, State}. 68 | 69 | is_authorized(#{method := <<"OPTIONS">>} =Req, State) -> {true, Req, State}; 70 | is_authorized(Req, #state{authconf = AuthConf} =State) -> 71 | try idp_http:decode_access_token(Req, AuthConf) of 72 | TokenPayload -> 73 | ?INFO_REPORT([{access_token, TokenPayload} | idp_http_log:format_request(Req)]), 74 | {true, Req, State#state{authm = TokenPayload}} 75 | catch 76 | T:R -> 77 | ?ERROR_REPORT(idp_http_log:format_unauthenticated_request(Req), T, R), 78 | {{false, idp_http:access_token_type()}, Req, State} 79 | end. 80 | 81 | forbidden(#{method := <<"OPTIONS">>} =Req, State) -> {false, Req, State}; 82 | forbidden(Req, #state{key = Key, authm = AuthM, rdesc = Rdesc} =State) -> 83 | try idp:authorize_subject(Key, AuthM, Rdesc) of 84 | {ok, Skey, #{write := true}} -> {false, Req, State#state{key = Skey}}; 85 | _ -> {true, Req, State} 86 | catch T:R -> 87 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 88 | {stop, cowboy_req:reply(422, Req), State} 89 | end. 90 | 91 | resource_exists(Req, #state{key = Key, rdesc = Rdesc} =State) -> 92 | try idp_account:read(Key, Rdesc) of 93 | {ok, A} -> {true, Req, State#state{p = A}}; 94 | _ -> {false, Req, State} 95 | catch T:R -> 96 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 97 | {stop, cowboy_req:reply(422, Req), State} 98 | end. 99 | 100 | content_types_provided(Req, State) -> 101 | Handlers = [{{<<"application">>, <<"json">>, '*'}, to_json}], 102 | {Handlers, Req, State}. 103 | 104 | allowed_methods(Req, State) -> 105 | Methods = [<<"GET">>, <<"OPTIONS">>], 106 | {Methods, Req, State}. 107 | 108 | options(Req0, State) -> 109 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"GET">>, Req0), 110 | Req2 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"authorization">>, Req1), 111 | Req3 = cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, <<"true">>, Req2), 112 | {ok, Req3, State}. 113 | 114 | %% ============================================================================= 115 | %% Content callbacks 116 | %% ============================================================================= 117 | 118 | to_json(Req, #state{authkeys = AuthKeys, p = A} =State) -> 119 | idp_http:handle_response(Req, State, fun() -> 120 | jsx:encode(idp_account_auth:list(AuthKeys, A)) 121 | end). 122 | -------------------------------------------------------------------------------- /src/idp_httph_account_enabled.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httph_account_enabled). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% REST handler callbacks 30 | -export([ 31 | init/2, 32 | is_authorized/2, 33 | forbidden/2, 34 | resource_exists/2, 35 | delete_resource/2, 36 | content_types_accepted/2, 37 | content_types_provided/2, 38 | allowed_methods/2, 39 | options/2 40 | ]). 41 | 42 | %% Content callbacks 43 | -export([ 44 | from_any/2, 45 | to_none/2 46 | ]). 47 | 48 | %% Types 49 | -record(state, { 50 | rdesc :: map(), 51 | authconf :: map(), 52 | key = undefined :: undefined | iodata(), 53 | authm = #{} :: map(), 54 | r = undefined :: undefined | riakauth_account:account() 55 | }). 56 | 57 | %% ============================================================================= 58 | %% REST handler callbacks 59 | %% ============================================================================= 60 | 61 | init(Req, Opts) -> 62 | #{authentication := AuthConf, resources := Rdesc} = Opts, 63 | State = 64 | #state{ 65 | rdesc = Rdesc, 66 | authconf = AuthConf, 67 | key = cowboy_req:binding(key, Req)}, 68 | {cowboy_rest, Req, State}. 69 | 70 | is_authorized(#{method := <<"OPTIONS">>} =Req, State) -> {true, Req, State}; 71 | is_authorized(Req, #state{authconf = AuthConf} =State) -> 72 | try idp_http:decode_access_token(Req, AuthConf) of 73 | TokenPayload -> 74 | ?INFO_REPORT([{access_token, TokenPayload} | idp_http_log:format_request(Req)]), 75 | {true, Req, State#state{authm = TokenPayload}} 76 | catch 77 | T:R -> 78 | ?ERROR_REPORT(idp_http_log:format_unauthenticated_request(Req), T, R), 79 | {{false, idp_http:access_token_type()}, Req, State} 80 | end. 81 | 82 | forbidden(#{method := <<"OPTIONS">>} =Req, State) -> {false, Req, State}; 83 | forbidden(Req, #state{key = Key, authm = AuthM, rdesc = Rdesc} =State) -> 84 | try idp:authorize_admin(Key, AuthM, Rdesc) of 85 | {ok, Skey, #{write := true}} -> {false, Req, State#state{key = Skey}}; 86 | _ -> {true, Req, State} 87 | catch T:R -> 88 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 89 | {stop, cowboy_req:reply(422, Req), State} 90 | end. 91 | 92 | resource_exists(#{method := Method} =Req, #state{key = Key, rdesc = Rdesc} =State) -> 93 | try idp_account:read(Key, Rdesc, read_options(Method)) of 94 | {ok, A} -> {true, Req, State#state{r = A}}; 95 | _ -> {false, Req, State} 96 | catch T:R -> 97 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 98 | {stop, cowboy_req:reply(422, Req), State} 99 | end. 100 | 101 | delete_resource(Req, #state{key = Akey, r = A, rdesc = Rdesc} =State) -> 102 | idp_http:handle_response(Req, State, fun() -> 103 | idp_account:disable(Akey, A, Rdesc) 104 | end). 105 | 106 | content_types_accepted(Req, State) -> 107 | Handlers = [{'*', from_any}], 108 | {Handlers, Req, State}. 109 | 110 | content_types_provided(Req, State) -> 111 | Handlers = [{{<<"text">>, <<"plain">>, '*'}, to_none}], 112 | {Handlers, Req, State}. 113 | 114 | allowed_methods(Req, State) -> 115 | Methods = [<<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], 116 | {Methods, Req, State}. 117 | 118 | options(Req0, State) -> 119 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"GET, PUT, DELETE">>, Req0), 120 | Req2 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"authorization">>, Req1), 121 | Req3 = cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, <<"true">>, Req2), 122 | {ok, Req3, State}. 123 | 124 | %% ============================================================================= 125 | %% Content callbacks 126 | %% ============================================================================= 127 | 128 | from_any(Req, #state{r = undefined} =State) -> 129 | {stop, cowboy_req:reply(404, Req), State}; 130 | from_any(Req, #state{key = Akey, r = A, rdesc = Rdesc} =State) -> 131 | idp_http:handle_response(Req, State, fun() -> 132 | idp_account:enable(Akey, A, Rdesc) 133 | end). 134 | 135 | to_none(Req0, #state{r = A} =State) -> 136 | Req1 = 137 | case idp_account:is_enabled(A) of 138 | true -> cowboy_req:reply(204, Req0); 139 | _ -> cowboy_req:reply(404, Req0) 140 | end, 141 | {stop, Req1, State}. 142 | 143 | %% ============================================================================= 144 | %% Internal functions 145 | %% ============================================================================= 146 | 147 | %% We use strict quorum (pr=quorum) for create or update operations 148 | %% on account's 'enable' property and sloppy quorum for read operations. 149 | -spec read_options(binary()) -> [proplists:property()]. 150 | read_options(<<"GET">>) -> []; 151 | read_options(<<"PUT">>) -> [{pr, quorum}]; 152 | read_options(<<"DELETE">>) -> [{pr, quorum}]. 153 | -------------------------------------------------------------------------------- /src/idp_httph_account_refresh.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httph_account_refresh). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% REST handler callbacks 30 | -export([ 31 | init/2, 32 | is_authorized/2, 33 | forbidden/2, 34 | content_types_accepted/2, 35 | allowed_methods/2, 36 | options/2 37 | ]). 38 | 39 | %% Content callbacks 40 | -export([ 41 | from_any/2 42 | ]). 43 | 44 | %% Types 45 | -record(state, { 46 | rdesc :: map(), 47 | tokens :: map(), 48 | authconf :: map(), 49 | key :: iodata(), 50 | authm = #{} :: map() 51 | }). 52 | 53 | %% ============================================================================= 54 | %% REST handler callbacks 55 | %% ============================================================================= 56 | 57 | init(Req, Opts) -> 58 | #{authentication := AuthConf, tokens := Tokens, resources := Rdesc} = Opts, 59 | State = 60 | #state{ 61 | rdesc = Rdesc, 62 | tokens = Tokens, 63 | authconf = AuthConf, 64 | key = cowboy_req:binding(key, Req)}, 65 | {cowboy_rest, Req, State}. 66 | 67 | is_authorized(#{method := <<"OPTIONS">>} =Req, State) -> {true, Req, State}; 68 | is_authorized(Req, #state{authconf = AuthConf, rdesc = Rdesc} =State) -> 69 | try idp_http:decode_refresh_token(Req, AuthConf, Rdesc) of 70 | TokenPayload -> 71 | ?INFO_REPORT([{access_token, TokenPayload} | idp_http_log:format_request(Req)]), 72 | {true, Req, State#state{authm = TokenPayload}} 73 | catch 74 | T:R -> 75 | ?ERROR_REPORT(idp_http_log:format_unauthenticated_request(Req), T, R), 76 | {{false, idp_http:access_token_type()}, Req, State} 77 | end. 78 | 79 | forbidden(#{method := <<"OPTIONS">>} =Req, State) -> {false, Req, State}; 80 | forbidden(Req, #state{key = Key, authm = AuthM, rdesc = Rdesc} =State) -> 81 | try idp:authorize_subject(Key, AuthM, Rdesc) of 82 | {ok, Skey, #{write := true}} -> {false, Req, State#state{key = Skey}}; 83 | _ -> {true, Req, State} 84 | catch T:R -> 85 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 86 | {stop, cowboy_req:reply(422, Req), State} 87 | end. 88 | 89 | content_types_accepted(Req, State) -> 90 | Handlers = [{'*', from_any}], 91 | {Handlers, Req, State}. 92 | 93 | allowed_methods(Req, State) -> 94 | Methods = [<<"POST">>, <<"OPTIONS">>], 95 | {Methods, Req, State}. 96 | 97 | options(Req0, State) -> 98 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"POST">>, Req0), 99 | Req2 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"authorization">>, Req1), 100 | Req3 = cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, <<"true">>, Req2), 101 | {ok, Req3, State}. 102 | 103 | %% ============================================================================= 104 | %% Content callbacks 105 | %% ============================================================================= 106 | 107 | from_any(Req, #state{key = Key, tokens = Tokens, rdesc = Rdesc} =State) -> 108 | idp_http:handle_response(Req, State, fun() -> 109 | idp_account:refresh_access_token(Key, Tokens, Rdesc) 110 | end). 111 | -------------------------------------------------------------------------------- /src/idp_httph_account_revoke.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httph_account_revoke). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% REST handler callbacks 30 | -export([ 31 | init/2, 32 | is_authorized/2, 33 | forbidden/2, 34 | content_types_accepted/2, 35 | allowed_methods/2, 36 | options/2 37 | ]). 38 | 39 | %% Content callbacks 40 | -export([ 41 | from_any/2 42 | ]). 43 | 44 | %% Types 45 | -record(state, { 46 | rdesc :: map(), 47 | tokens :: map(), 48 | authconf :: map(), 49 | key :: iodata(), 50 | authm = #{} :: map() 51 | }). 52 | 53 | %% ============================================================================= 54 | %% REST handler callbacks 55 | %% ============================================================================= 56 | 57 | init(Req, Opts) -> 58 | #{authentication := AuthConf, tokens := Tokens, resources := Rdesc} = Opts, 59 | State = 60 | #state{ 61 | rdesc = Rdesc, 62 | tokens = Tokens, 63 | authconf = AuthConf, 64 | key = cowboy_req:binding(key, Req)}, 65 | {cowboy_rest, Req, State}. 66 | 67 | is_authorized(#{method := <<"OPTIONS">>} =Req, State) -> {true, Req, State}; 68 | is_authorized(Req, #state{authconf = AuthConf, rdesc = Rdesc} =State) -> 69 | try idp_http:decode_refresh_token(Req, AuthConf, Rdesc) of 70 | TokenPayload -> 71 | ?INFO_REPORT([{access_token, TokenPayload} | idp_http_log:format_request(Req)]), 72 | {true, Req, State#state{authm = TokenPayload}} 73 | catch 74 | T:R -> 75 | ?ERROR_REPORT(idp_http_log:format_unauthenticated_request(Req), T, R), 76 | {{false, idp_http:access_token_type()}, Req, State} 77 | end. 78 | 79 | forbidden(#{method := <<"OPTIONS">>} =Req, State) -> {false, Req, State}; 80 | forbidden(Req, #state{key = Key, authm = AuthM, rdesc = Rdesc} =State) -> 81 | try idp:authorize_subject(Key, AuthM, Rdesc) of 82 | {ok, Skey, #{write := true}} -> {false, Req, State#state{key = Skey}}; 83 | _ -> {true, Req, State} 84 | catch T:R -> 85 | ?ERROR_REPORT(idp_http_log:format_request(Req), T, R), 86 | {stop, cowboy_req:reply(422, Req), State} 87 | end. 88 | 89 | content_types_accepted(Req, State) -> 90 | Handlers = [{'*', from_any}], 91 | {Handlers, Req, State}. 92 | 93 | allowed_methods(Req, State) -> 94 | Methods = [<<"POST">>, <<"OPTIONS">>], 95 | {Methods, Req, State}. 96 | 97 | options(Req0, State) -> 98 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"POST">>, Req0), 99 | Req2 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"authorization">>, Req1), 100 | Req3 = cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, <<"true">>, Req2), 101 | {ok, Req3, State}. 102 | 103 | %% ============================================================================= 104 | %% Content callbacks 105 | %% ============================================================================= 106 | 107 | from_any(Req, #state{key = Key, tokens = Tokens, rdesc = Rdesc} =State) -> 108 | idp_http:handle_response(Req, State, fun() -> 109 | idp_account:revoke_refresh_token(Key, Tokens, Rdesc) 110 | end). 111 | -------------------------------------------------------------------------------- /src/idp_httph_oauth2_token.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httph_oauth2_token). 26 | 27 | -include("idp_log.hrl"). 28 | 29 | %% REST handler callbacks 30 | -export([ 31 | init/2, 32 | is_authorized/2, 33 | forbidden/2, 34 | content_types_accepted/2, 35 | allowed_methods/2, 36 | options/2 37 | ]). 38 | 39 | %% Content callbacks 40 | -export([ 41 | from_wform/2, 42 | from_json/2 43 | ]). 44 | 45 | %% Types 46 | -record(state, { 47 | op :: create | link, 48 | rdesc :: map(), 49 | tokens :: map(), 50 | authconf :: map(), 51 | idpsconf :: map(), 52 | authm = #{} :: map() 53 | }). 54 | 55 | %% ============================================================================= 56 | %% REST handler callbacks 57 | %% ============================================================================= 58 | 59 | init(Req, Opts) -> 60 | #{operation := Op, 61 | resources := Rdesc, 62 | tokens := Tokens, 63 | authentication := AuthConf, 64 | identity_providers := IdpsConf} = Opts, 65 | State = 66 | #state{ 67 | op = Op, 68 | rdesc = Rdesc, 69 | tokens = Tokens, 70 | authconf = AuthConf, 71 | idpsconf = IdpsConf}, 72 | {cowboy_rest, Req, State}. 73 | 74 | is_authorized(#{method := <<"OPTIONS">>} =Req, State) -> {true, Req, State}; 75 | is_authorized(Req, #state{op = create} =State) -> {true, Req, State}; 76 | is_authorized(Req, #state{op = link, authconf = AuthConf} =State) -> 77 | try idp_http:decode_access_token(Req, AuthConf) of 78 | TokenPayload -> 79 | ?INFO_REPORT([{access_token, TokenPayload}|idp_http_log:format_request(Req)]), 80 | {true, Req, State#state{authm = TokenPayload}} 81 | catch 82 | T:R -> 83 | ?ERROR_REPORT(idp_http_log:format_unauthenticated_request(Req), T, R), 84 | {{false, idp_http:access_token_type()}, Req, State} 85 | end. 86 | 87 | forbidden(#{method := <<"OPTIONS">>} =Req, State) -> {false, Req, State}; 88 | forbidden(Req, #state{op = create} =State) -> {false, Req, State}; 89 | forbidden(Req, #state{op = link, authm = #{<<"sub">> := _}} =State) -> {false, Req, State}; 90 | forbidden(Req, State) -> {true, Req, State}. 91 | 92 | content_types_accepted(Req, State) -> 93 | Handlers = 94 | [ {{<<"application">>, <<"json">>, '*'}, from_json}, 95 | {{<<"application">>, <<"x-www-form-urlencoded">>, '*'}, from_wform} ], 96 | {Handlers, Req, State}. 97 | 98 | allowed_methods(Req, State) -> 99 | Methods = [<<"POST">>, <<"OPTIONS">>], 100 | {Methods, Req, State}. 101 | 102 | options(Req0, State) -> 103 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"POST">>, Req0), 104 | Req2 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"authorization, content-length, content-type">>, Req1), 105 | Req3 = cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, <<"true">>, Req2), 106 | {ok, Req3, State}. 107 | 108 | %% ============================================================================= 109 | %% Content callbacks 110 | %% ============================================================================= 111 | 112 | %% According to RFC 6749 - The OAuth 2.0 Authorization Framework 113 | %% 5.2. Issuing an Access Token. Error Response 114 | %% https://tools.ietf.org/html/rfc6749#section-5.2 115 | from_wform(Req, #state{rdesc = Rdesc, tokens = Tokens, idpsconf = IdpsConf, authm = AuthM, op = Op} =State) -> 116 | FailureContentType = <<"application/x-www-form-urlencoded">>, 117 | SuccessContentType = <<"application/json">>, 118 | idp_http:control_payload( 119 | Req, State, #{}, 120 | fun(Payload, Sreq) -> 121 | L = cow_qs:parse_qs(Payload), 122 | {_, <<"client_credentials">>} = lists:keyfind(<<"grant_type">>, 1, L), 123 | {_, ClientToken} = lists:keyfind(<<"client_token">>, 1, L), 124 | #{options := 125 | #{key := Key, 126 | alg := Alg, 127 | verify_options := Opts}} = IdpsConf, 128 | ClientTokenPayload = jose_jws_compact:decode(ClientToken, Alg, Key, Opts#{parse_payload => map}), 129 | idp_http:control_response( 130 | Sreq, State, SuccessContentType, 131 | fun() -> 132 | case Op of 133 | create -> idp_account:create(ClientTokenPayload, Rdesc, Tokens, IdpsConf); 134 | link -> idp_account:link(ClientTokenPayload, AuthM, Rdesc, Tokens, IdpsConf) 135 | end 136 | end, 137 | fun(_Fpayload, Freq, Fstate) -> 138 | RespHeaders = #{<<"content-type">> => FailureContentType}, 139 | RespPayload = <<"error=invalid_client">>, 140 | {stop, cowboy_req:reply(400, RespHeaders, RespPayload, Freq), Fstate} 141 | end) 142 | end, 143 | fun(_Fpayload, Freq, Fstate) -> 144 | RespHeaders = #{<<"content-type">> => FailureContentType}, 145 | RespPayload = <<"error=invalid_request">>, 146 | {stop, cowboy_req:reply(400, RespHeaders, RespPayload, Freq), Fstate} 147 | end). 148 | 149 | %% According to RFC 6749 - The OAuth 2.0 Authorization Framework 150 | %% 5.2. Issuing an Access Token. Error Response 151 | %% https://tools.ietf.org/html/rfc6749#section-5.2 152 | from_json(Req0, #state{rdesc = Rdesc, tokens = Tokens, idpsconf = IdpsConf, authm = AuthM, op = Op} =State) -> 153 | ContentType = <<"application/json">>, 154 | idp_http:control_payload( 155 | Req0, State, #{}, 156 | fun(Payload, Req1) -> 157 | L = jsx:decode(Payload), 158 | {_, <<"client_credentials">>} = lists:keyfind(<<"grant_type">>, 1, L), 159 | {_, ClientToken} = lists:keyfind(<<"client_token">>, 1, L), 160 | #{options := 161 | #{key := Key, 162 | alg := Alg, 163 | verify_options := Opts}} = IdpsConf, 164 | ClientTokenPayload = jose_jws_compact:decode(ClientToken, Alg, Key, Opts#{parse_payload => map}), 165 | idp_http:control_response( 166 | Req1, State, ContentType, 167 | fun() -> 168 | case Op of 169 | create -> idp_account:create(ClientTokenPayload, Rdesc, Tokens, IdpsConf); 170 | link -> idp_account:link(ClientTokenPayload, AuthM, Rdesc, Tokens, IdpsConf) 171 | end 172 | end, 173 | fun(_Fpayload, Freq, Fstate) -> 174 | RespHeaders = #{<<"content-type">> => ContentType}, 175 | RespPayload = idp_http:encode_payload(ContentType, #{error => invalid_client}), 176 | {stop, cowboy_req:reply(400, RespHeaders, RespPayload, Freq), Fstate} 177 | end) 178 | end, 179 | fun(Fpayload, Freq, Fstate) -> 180 | RespHeaders = #{<<"content-type">> => ContentType}, 181 | RespPayload = idp_http:encode_payload(ContentType, #{error => invalid_request, error_description => Fpayload}), 182 | {stop, cowboy_req:reply(400, RespHeaders, RespPayload, Freq), Fstate} 183 | end). 184 | -------------------------------------------------------------------------------- /src/idp_httpm_cors.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_httpm_cors). 26 | 27 | %% Middleware callbacks 28 | -export([ 29 | execute/2 30 | ]). 31 | 32 | %% Definitions 33 | -define(DEFAULT_ALLOWED_ORIGINS, '*'). 34 | 35 | %% ============================================================================= 36 | %% Middleware callbacks 37 | %% ============================================================================= 38 | 39 | -spec execute(Req, Env) -> {ok | stop, Req, Env} when Req :: cowboy_req:req(), Env :: any(). 40 | execute(#{headers := #{<<"origin">> := HeaderVal}} =Req0, Env) -> 41 | [Origin] = cow_http_hd:parse_origin(HeaderVal), 42 | case check_origin(Origin, maps:get(allowed_origins, Env, ?DEFAULT_ALLOWED_ORIGINS)) of 43 | true -> 44 | Req1 = cowboy_req:set_resp_header(<<"access-control-allow-origin">>, HeaderVal, Req0), 45 | Req2 = cowboy_req:set_resp_header(<<"vary">>, <<"Origin">>, Req1), 46 | Req3 = maybe_set_access_control_max_age_header(Req2, Env), 47 | {ok, Req3, Env}; 48 | _ -> 49 | {ok, Req0, Env} 50 | end; 51 | execute(Req, Env) -> 52 | {ok, Req, Env}. 53 | 54 | %% ============================================================================= 55 | %% Internal functions 56 | %% ============================================================================= 57 | 58 | -spec check_origin(Origin, Origin | [Origin] | '*') -> boolean() when Origin :: {binary(), binary(), 0..65535} | reference(). 59 | check_origin(Val, '*') when is_reference(Val) -> true; 60 | check_origin(_, '*') -> true; 61 | check_origin(Val, Val) -> true; 62 | check_origin(Val, L) when is_list(L) -> lists:member(Val, L); 63 | check_origin(_, _) -> false. 64 | 65 | maybe_set_access_control_max_age_header(#{method := <<"OPTIONS">>} =Req, #{preflight_request_max_age := <<"0">>}) -> 66 | Req; 67 | maybe_set_access_control_max_age_header(#{method := <<"OPTIONS">>} =Req, #{preflight_request_max_age := Val}) -> 68 | cowboy_req:set_resp_header(<<"access-control-max-age">>, Val, Req); 69 | maybe_set_access_control_max_age_header(Req, _Env) -> 70 | Req. 71 | -------------------------------------------------------------------------------- /src/idp_log.hrl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -define(INFO_REPORT(L), 26 | error_logger:info_report( 27 | [ {module, ?MODULE}, 28 | {function, ?FUNCTION_NAME}, 29 | {function_arity, ?FUNCTION_ARITY} 30 | | L ])). 31 | 32 | -define(WARNING_REPORT(L), 33 | error_logger:warning_report( 34 | [ {module, ?MODULE}, 35 | {function, ?FUNCTION_NAME}, 36 | {function_arity, ?FUNCTION_ARITY} 37 | | L ])). 38 | 39 | -define(ERROR_REPORT(L), 40 | error_logger:error_report( 41 | [ {module, ?MODULE}, 42 | {function, ?FUNCTION_NAME}, 43 | {function_arity, ?FUNCTION_ARITY} 44 | | L ])). 45 | 46 | -define(ERROR_REPORT(L, Class, Reason), 47 | error_logger:error_report( 48 | [ {module, ?MODULE}, 49 | {function, ?FUNCTION_NAME}, 50 | {function_arity, ?FUNCTION_ARITY}, 51 | {stacktrace, erlang:get_stacktrace()}, 52 | {exception_class, Class}, 53 | {exception_reason, Reason} 54 | | L ])). 55 | -------------------------------------------------------------------------------- /src/idp_streamh_log.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_streamh_log). 26 | -behaviour(cowboy_stream). 27 | 28 | -include("idp_log.hrl"). 29 | 30 | %% Stream handler callbacks 31 | -export([ 32 | init/3, 33 | data/4, 34 | info/3, 35 | terminate/3, 36 | early_error/5 37 | ]). 38 | 39 | %% Types 40 | -record(state, { 41 | next :: any(), 42 | cat :: non_neg_integer(), 43 | ctx :: idp_http_log:kvlist() 44 | }). 45 | 46 | %% ============================================================================= 47 | %% Stream handler callbacks 48 | %% ============================================================================= 49 | 50 | init(StreamId, Req, Opts) -> 51 | _ = exometer:update([idp,request,http,count], 1), 52 | 53 | StartedAt = idp:unix_time_us(), 54 | Context = [{http_started_at, StartedAt} | idp_http_log:format_request(Req)], 55 | ?INFO_REPORT(Context), 56 | 57 | {Ncmd, Nstate} = cowboy_stream:init(StreamId, Req, Opts), 58 | {Ncmd, #state{next = Nstate, cat = StartedAt, ctx = Context}}. 59 | 60 | data(StreamId, IsFin, Data, #state{next = Nstate0} =State) -> 61 | {Ncmd, Nstate1} = cowboy_stream:data(StreamId, IsFin, Data, Nstate0), 62 | {Ncmd, State#state{next = Nstate1}}. 63 | 64 | info(StreamId, Response, #state{next = Nstate0, cat = StartedAt, ctx = Context} =State) -> 65 | _ = 66 | case Response of 67 | {response, Status, Headers, _Body} -> handle_response(StartedAt, Status, Headers, Context); 68 | {headers, Status, Headers} -> handle_response(StartedAt, Status, Headers, Context); 69 | _ -> ignore 70 | end, 71 | {Ncmd, Nstate1} = cowboy_stream:info(StreamId, Response, Nstate0), 72 | {Ncmd, State#state{next = Nstate1}}. 73 | 74 | terminate(StreamId, Reason, #state{next = Nstate, ctx = Context, cat = StartedAt}) -> 75 | handle_terminate(Reason, StartedAt, Context), 76 | cowboy_stream:terminate(StreamId, Reason, Nstate). 77 | 78 | early_error(StreamId, Reason, PartialReq, Resp, Opts) -> 79 | ?ERROR_REPORT(idp_http_log:format_request(PartialReq)), 80 | cowboy_stream:early_error(StreamId, Reason, PartialReq, Resp, Opts). 81 | 82 | %% ============================================================================= 83 | %% Internal function 84 | %% ============================================================================= 85 | 86 | -spec handle_response(non_neg_integer(), integer(), map(), idp_http_log:kvlist()) -> ok. 87 | handle_response(StartedAt, Status, Headers, Context) -> 88 | Duration = duration(StartedAt), 89 | _ = exometer:update([idp,request,http,duration], Duration), 90 | ?INFO_REPORT([{http_duration, Duration} | idp_http_log:format_response(Status, Headers, Context)]). 91 | 92 | -spec handle_terminate(atom(), non_neg_integer(), idp_http_log:kvlist()) -> ok. 93 | handle_terminate(normal, _StartedAt, _Context) -> ok; 94 | handle_terminate(shutdown, _StartedAt, _Context) -> ok; 95 | handle_terminate({shutdown, _}, _StartedAt, _Context) -> ok; 96 | handle_terminate(Reason, StartedAt, Context) -> 97 | Duration = duration(StartedAt), 98 | _ = exometer:update([idp,http,duration], Duration), 99 | ?ERROR_REPORT([{exception_reason, Reason}, {http_duration, Duration} | Context]). 100 | 101 | -spec duration(non_neg_integer()) -> non_neg_integer(). 102 | duration(StartedAt) -> 103 | Now = idp:unix_time_us(), 104 | Now - StartedAt. 105 | -------------------------------------------------------------------------------- /src/idp_sup.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_sup). 26 | -behaviour(supervisor). 27 | 28 | %% API 29 | -export([ 30 | start_link/0 31 | ]). 32 | 33 | %% Supervisor callbacks 34 | -export([ 35 | init/1 36 | ]). 37 | 38 | %% ============================================================================= 39 | %% API 40 | %% ============================================================================= 41 | 42 | start_link() -> 43 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 44 | 45 | %% ============================================================================= 46 | %% Supervisor callbacks 47 | %% ============================================================================= 48 | 49 | init([]) -> 50 | Flags = #{strategy => one_for_one}, 51 | Procs = [riakc_pool:child_spec(PoolDesc) || PoolDesc <- idp:riak_connection_pools()], 52 | {ok, {Flags, Procs}}. 53 | -------------------------------------------------------------------------------- /test/account_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(account_SUITE). 26 | -include_lib("common_test/include/ct.hrl"). 27 | 28 | -compile(export_all). 29 | 30 | %% ============================================================================= 31 | %% Common Test callbacks 32 | %% ============================================================================= 33 | 34 | all() -> 35 | application:ensure_all_started(idp), 36 | application:ensure_all_started(gun), 37 | [{group, account}]. 38 | 39 | groups() -> 40 | [{account, [parallel], ct_helper:all(?MODULE)}]. 41 | 42 | init_per_suite(Config) -> 43 | idp_cth:init_config() ++ Config. 44 | 45 | init_per_testcase(_Test, Config) -> 46 | #{account := #{pool := KVpool}} = idp:resources(), 47 | Accounts = [admin, user, user_other, anonymous], 48 | 49 | KVpid = riakc_pool:lock(KVpool), 50 | %% Creating accounts 51 | Admin = idp_cli_account:create(KVpid, #{acl => [{<<"admin">>, riakacl_group:new_dt()}]}, #{}), 52 | User = idp_cli_account:create(KVpid, #{}, #{}), 53 | UserOther = idp_cli_account:create(KVpid, #{}, #{}), 54 | riakc_pool:unlock(KVpool, KVpid), 55 | 56 | [ {admin, Admin}, 57 | {user, User}, 58 | {user_other, UserOther}, 59 | {accounts, Accounts} 60 | | Config ]. 61 | 62 | end_per_testcase(_Test, Config) -> 63 | Config. 64 | 65 | end_per_suite(Config) -> 66 | Config. 67 | 68 | %% ============================================================================= 69 | %% Tests 70 | %% ============================================================================= 71 | 72 | %% Returns the specified account. 73 | %% The 404 'Not Found' error is returned for accounts that don't exist. 74 | read(Config) -> 75 | #{id := Akey} = ?config(admin, Config), 76 | AkeyNotExist = idp:make_uuid(), 77 | AuthorizationH = idp_cth:authorization_header(admin, Config), 78 | Test = 79 | [ %% account me 80 | {<<"/api/v1/accounts/me">>, 200}, 81 | %% account exists 82 | {[<<"/api/v1/accounts/">>, Akey], 200}, 83 | %% account doesn't exist 84 | {[<<"/api/v1/accounts/">>, AkeyNotExist], 404} ], 85 | 86 | [begin 87 | Pid = idp_cth:gun_open(Config), 88 | Ref = gun:request(Pid, <<"GET">>, Path, [AuthorizationH]), 89 | case Status of 90 | 200 -> {200, _Hs, #{<<"id">> := _}} = idp_cth:gun_await_json(Pid, Ref); 91 | 404 -> {404, _Hs, <<>>} = idp_cth:gun_await(Pid, Ref) 92 | end 93 | end || {Path, Status} <- Test]. 94 | 95 | %% Access is granted only for accounts` owners 96 | %% or members of 'admin' (predefined) group. 97 | read_permissions(Config) -> 98 | #{id := Akey} = ?config(user, Config), 99 | Path = [<<"/api/v1/accounts/">>, Akey], 100 | Accounts = ?config(accounts, Config), 101 | Test = 102 | [ {200, [user]}, 103 | {200, [admin]}, 104 | {403, Accounts -- [user, admin]} ], 105 | 106 | [begin 107 | [begin 108 | Pid = idp_cth:gun_open(Config), 109 | Ref = gun:request(Pid, <<"GET">>, Path, idp_cth:authorization_headers(A, Config)), 110 | {St, _Hs, _Body} = idp_cth:gun_await(Pid, Ref) 111 | end || A <- As] 112 | end || {St, As} <- Test]. 113 | 114 | %% Removes the specified account. 115 | %% Returns the removed account. 116 | %% The 404 'Not Found' error is returned for accounts that don't exist. 117 | delete(Config) -> 118 | #{id := Akey} = ?config(user, Config), 119 | AkeyNotExist = idp:make_uuid(), 120 | Test = 121 | [ %% me 122 | {<<"/api/v1/accounts/me">>, user_other, 200}, 123 | %% account exist 124 | {[<<"/api/v1/accounts/">>, Akey], admin, 200}, 125 | %% account doesn't exist 126 | {[<<"/api/v1/accounts/">>, AkeyNotExist], admin, 404} ], 127 | 128 | [begin 129 | AuthorizationH = idp_cth:authorization_header(A, Config), 130 | Pid = idp_cth:gun_open(Config), 131 | Ref = gun:request(Pid, <<"DELETE">>, Path, [AuthorizationH]), 132 | case Status of 133 | 200 -> {200, _Hs, #{<<"id">> := _}} = idp_cth:gun_await_json(Pid, Ref); 134 | 404 -> {404, _Hs, <<>>} = idp_cth:gun_await(Pid, Ref) 135 | end 136 | end || {Path, A, Status} <- Test]. 137 | 138 | %% Access is granted only for accounts` owners 139 | %% or members of 'admin' (predefined) group. 140 | delete_permissions_owner(Config) -> do_delete_permissions(200, [user], Config). 141 | delete_permissions_admin(Config) -> do_delete_permissions(200, [admin], Config). 142 | delete_permissions(Config) -> do_delete_permissions(403, ?config(accounts, Config) -- [user, admin], Config). 143 | 144 | do_delete_permissions(Status, Accounts, Config) -> 145 | #{id := Akey} = ?config(user, Config), 146 | Path = [<<"/api/v1/accounts/">>, Akey], 147 | 148 | [begin 149 | Pid = idp_cth:gun_open(Config), 150 | Ref = gun:request(Pid, <<"DELETE">>, Path, idp_cth:authorization_headers(A, Config)), 151 | {Status, _Hs, _Body} = idp_cth:gun_await(Pid, Ref) 152 | end || A <- Accounts]. 153 | -------------------------------------------------------------------------------- /test/account_auth_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(account_auth_SUITE). 26 | -include_lib("common_test/include/ct.hrl"). 27 | 28 | -compile(export_all). 29 | 30 | %% ============================================================================= 31 | %% Common Test callbacks 32 | %% ============================================================================= 33 | 34 | all() -> 35 | application:ensure_all_started(idp), 36 | application:ensure_all_started(gun), 37 | [{group, account_auth}]. 38 | 39 | groups() -> 40 | [{account_auth, [parallel], ct_helper:all(?MODULE)}]. 41 | 42 | init_per_suite(Config) -> 43 | idp_cth:init_config() ++ Config. 44 | 45 | init_per_testcase(_Test, Config) -> 46 | #{account := #{pool := KVpool}} = idp:resources(), 47 | HandleData = fun(Data) -> Data end, 48 | I = [<<"oauth2">>, <<"example">>, idp_cth:make_uid()], 49 | Accounts = [user, admin, anonymous], 50 | 51 | KVpid = riakc_pool:lock(KVpool), 52 | Admin = idp_cli_account:create(KVpid, #{acl => [{<<"admin">>, riakacl_group:new_dt()}], identities => [I]}, #{}), 53 | User = idp_cli_account:create(KVpid, #{identities => [I]}, #{}), 54 | UserNoIdentity = idp_cli_account:create(KVpid, #{}, #{}), 55 | riakc_pool:unlock(KVpool, KVpid), 56 | 57 | [ {admin, Admin}, 58 | {user, User}, 59 | {user_noidentities, UserNoIdentity}, 60 | {identity, I}, 61 | {accounts, Accounts} 62 | | Config ]. 63 | 64 | end_per_testcase(_Test, Config) -> 65 | Config. 66 | 67 | end_per_suite(Config) -> 68 | Config. 69 | 70 | %% ============================================================================= 71 | %% Tests 72 | %% ============================================================================= 73 | 74 | %% Returns a list of account's identities. 75 | %% An empty list is returned for accounts that don't have any linked identities. 76 | %% The 404 'Not Found' error is returned for accounts that don't exist. 77 | list(Config) -> 78 | #{id := Key, access_token := Token} = ?config(admin, Config), 79 | #{id := KeyNoIdentities} = ?config(user_noidentities, Config), 80 | KeyNotExist = idp:make_uuid(), 81 | AuthorizationH = {<<"authorization">>, [<<"Bearer ">>, Token]}, 82 | Test = 83 | [ %% account w/ identity 84 | {[<<"/api/v1/accounts/">>, Key, <<"/auth">>], 200}, 85 | %% account w/o identity 86 | {[<<"/api/v1/accounts/">>, KeyNoIdentities, <<"/auth">>], 200}, 87 | %% account doesn't exist 88 | {[<<"/api/v1/accounts/">>, KeyNotExist, <<"/auth">>], 404} ], 89 | 90 | Pid = idp_cth:gun_open(Config), 91 | [begin 92 | Ref = gun:request(Pid, <<"GET">>, Path, [AuthorizationH]), 93 | case Status of 94 | 404 -> {404, _Hs, _Body} = idp_cth:gun_await(Pid, Ref); 95 | 200 -> 96 | {200, _Hs, L} = idp_cth:gun_await_json(Pid, Ref), 97 | [#{<<"id">> := _} =Obj || Obj <- L] 98 | end 99 | end || {Path, Status} <- Test]. 100 | 101 | %% Access is granted only for accounts themselves or accounts that 102 | %% are members of 'admin' (predefined) group. 103 | list_permissions(Config) -> 104 | Accounts = ?config(accounts, Config), 105 | Test = 106 | [ {200, [user], <<"me">>}, 107 | {200, [user], maps:get(id, ?config(user, Config))}, 108 | {200, [admin], maps:get(id, ?config(user, Config))}, 109 | {403, Accounts -- [admin], maps:get(id, ?config(admin, Config))} ], 110 | 111 | Pid = idp_cth:gun_open(Config), 112 | [begin 113 | [begin 114 | Path = [<<"/api/v1/accounts/">>, Key, <<"/auth">>], 115 | Ref = gun:request(Pid, <<"GET">>, Path, idp_cth:authorization_headers(A, Config)), 116 | {St, _Hs, _Body} = idp_cth:gun_await(Pid, Ref) 117 | end || A <- As] 118 | end || {St, As, Key} <- Test]. 119 | 120 | %% Removes the specified identity. 121 | %% Returns the removed identity. 122 | %% The 404 'Not Found' error is returned for accounts or identies 123 | %% that don't exist. 124 | delete(Config) -> 125 | #{id := Key, access_token := Token} = ?config(admin, Config), 126 | #{id := KeyNoIdentities} = ?config(user_noidentities, Config), 127 | KeyNotExist = idp:make_uuid(), 128 | I = ?config(identity, Config), 129 | Ibin = idp_account_auth:identity(I), 130 | AuthorizationH = {<<"authorization">>, [<<"Bearer ">>, Token]}, 131 | Test = 132 | [ %% account w/ identity 133 | {[<<"/api/v1/accounts/">>, Key, <<"/auth/">>, Ibin], 200}, 134 | %% account w/o identity 135 | {[<<"/api/v1/accounts/">>, KeyNoIdentities, <<"/auth/">>, Ibin], 404}, 136 | %% account doesn't exist 137 | {[<<"/api/v1/accounts/">>, KeyNotExist, <<"/auth/">>, Ibin], 404} ], 138 | 139 | Pid = idp_cth:gun_open(Config), 140 | [begin 141 | Ref = gun:request(Pid, <<"DELETE">>, Path, [AuthorizationH]), 142 | case Status of 143 | 200 -> {200, _Hs, #{<<"id">> := Ibin}} = idp_cth:gun_await_json(Pid, Ref); 144 | 404 -> {404, _Hs, <<>>} = idp_cth:gun_await(Pid, Ref) 145 | end 146 | end || {Path, Status} <- Test]. 147 | 148 | %% Access is granted only for accounts themselves or accounts that 149 | %% are members of 'admin' (predefined) group. 150 | delete_permissions_owner_me(Config) -> do_delete_permissions(200, [user], <<"me">>, Config). 151 | delete_permissions_owner_id(Config) -> do_delete_permissions(200, [user], maps:get(id, ?config(user, Config)), Config). 152 | delete_permissions_admin(Config) -> do_delete_permissions(200, [admin], maps:get(id, ?config(user, Config)), Config). 153 | delete_permissions(Config) -> do_delete_permissions(403, ?config(accounts, Config) -- [admin], maps:get(id, ?config(admin, Config)), Config). 154 | 155 | do_delete_permissions(Status, Accounts, Key, Config) -> 156 | I = ?config(identity, Config), 157 | Ibin = idp_account_auth:identity(I), 158 | Path = [<<"/api/v1/accounts/">>, Key, <<"/auth/">>, Ibin], 159 | 160 | Pid = idp_cth:gun_open(Config), 161 | [begin 162 | Ref = gun:request(Pid, <<"DELETE">>, Path, idp_cth:authorization_headers(A, Config)), 163 | {Status, _Hs, _Body} = idp_cth:gun_await(Pid, Ref) 164 | end || A <- Accounts]. 165 | -------------------------------------------------------------------------------- /test/account_enabled_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(account_enabled_SUITE). 26 | -include_lib("common_test/include/ct.hrl"). 27 | 28 | -compile(export_all). 29 | 30 | %% ============================================================================= 31 | %% Common Test callbacks 32 | %% ============================================================================= 33 | 34 | all() -> 35 | application:ensure_all_started(idp), 36 | application:ensure_all_started(gun), 37 | [{group, account}]. 38 | 39 | groups() -> 40 | [{account, [parallel], ct_helper:all(?MODULE)}]. 41 | 42 | init_per_suite(Config) -> 43 | idp_cth:init_config() ++ Config. 44 | 45 | init_per_testcase(_Test, Config) -> 46 | #{account := #{pool := KVpool}} = idp:resources(), 47 | Accounts = [admin, user, user_disabled, anonymous], 48 | 49 | KVpid = riakc_pool:lock(KVpool), 50 | %% Creating accounts 51 | Admin = idp_cli_account:create(KVpid, #{acl => [{<<"admin">>, riakacl_group:new_dt()}]}, #{}), 52 | User = idp_cli_account:create(KVpid, #{}, #{}), 53 | UserDisabled = idp_cli_account:create(KVpid, #{enable => false}, #{}), 54 | riakc_pool:unlock(KVpool, KVpid), 55 | 56 | [ {admin, Admin}, 57 | {user, User}, 58 | {user_disabled, UserDisabled}, 59 | {accounts, Accounts} 60 | | Config ]. 61 | 62 | end_per_testcase(_Test, Config) -> 63 | Config. 64 | 65 | end_per_suite(Config) -> 66 | Config. 67 | 68 | %% ============================================================================= 69 | %% Tests 70 | %% ============================================================================= 71 | 72 | %% Retrieves a status of specified account: enabled or disabled. 73 | %% The 204 'No content' is returned if account is enabled. 74 | %% The 404 'Not Found' error is returned if account is disabled or don't exist. 75 | read(Config) -> 76 | #{id := Akey} = ?config(user, Config), 77 | #{id := AkeyDisabled} = ?config(user_disabled, Config), 78 | AkeyNotExist = idp:make_uuid(), 79 | AuthorizationH = idp_cth:authorization_header(admin, Config), 80 | Test = 81 | [ %% account me (admin) 82 | {<<"/api/v1/accounts/me/enabled">>, 204}, 83 | %% account exists 84 | {[<<"/api/v1/accounts/">>, Akey, <<"/enabled">>], 204}, 85 | %% disabled account 86 | {[<<"/api/v1/accounts/">>, AkeyDisabled, <<"/enabled">>], 404}, 87 | %% account doesn't exist 88 | {[<<"/api/v1/accounts/">>, AkeyNotExist, <<"/enabled">>], 404} ], 89 | 90 | [begin 91 | Pid = idp_cth:gun_open(Config), 92 | Ref = gun:request(Pid, <<"GET">>, Path, [AuthorizationH]), 93 | {Status, _Hs, <<>>} = idp_cth:gun_await(Pid, Ref) 94 | end || {Path, Status} <- Test]. 95 | 96 | %% Access is granted only for members of 'admin' (predefined) group. 97 | read_permissions(Config) -> 98 | #{id := Akey} = ?config(user, Config), 99 | Path = [<<"/api/v1/accounts/">>, Akey, <<"/enabled">>], 100 | Accounts = ?config(accounts, Config), 101 | Test = 102 | [ {204, [admin]}, 103 | {403, Accounts -- [admin]} ], 104 | 105 | [begin 106 | [begin 107 | Pid = idp_cth:gun_open(Config), 108 | Ref = gun:request(Pid, <<"GET">>, Path, idp_cth:authorization_headers(A, Config)), 109 | {St, _Hs, _Body} = idp_cth:gun_await(Pid, Ref) 110 | end || A <- As] 111 | end || {St, As} <- Test]. 112 | 113 | %% Enables the specified account. 114 | %% The 204 'No content' is returned on success. 115 | %% The 404 'Not Found' error is returned for accounts that don't exist. 116 | update(Config) -> 117 | #{id := Akey} = ?config(user, Config), 118 | #{id := AkeyDisabled} = ?config(user_disabled, Config), 119 | AkeyNotExist = idp:make_uuid(), 120 | AuthorizationH = idp_cth:authorization_header(admin, Config), 121 | Test = 122 | [ %% account me (admin) 123 | {<<"/api/v1/accounts/me/enabled">>, 204}, 124 | %% account exists 125 | {[<<"/api/v1/accounts/">>, Akey, <<"/enabled">>], 204}, 126 | %% disabled account 127 | {[<<"/api/v1/accounts/">>, AkeyDisabled, <<"/enabled">>], 204}, 128 | %% account doesn't exist 129 | {[<<"/api/v1/accounts/">>, AkeyNotExist, <<"/enabled">>], 404} ], 130 | 131 | [begin 132 | Pid = idp_cth:gun_open(Config), 133 | ct:log("curl -vk -XPUT https://localhost:8443~s -H'Authorization: ~s'~n", [iolist_to_binary(Path), begin {_, X} = AuthorizationH, iolist_to_binary(X) end]), 134 | Ref = gun:request(Pid, <<"PUT">>, Path, [AuthorizationH], <<>>), 135 | {Status, _Hs, <<>>} = idp_cth:gun_await(Pid, Ref) 136 | end || {Path, Status} <- Test]. 137 | 138 | %% Access is granted only for members of 'admin' (predefined) group. 139 | update_permissions(Config) -> 140 | #{id := Akey} = ?config(user, Config), 141 | Path = [<<"/api/v1/accounts/">>, Akey, <<"/enabled">>], 142 | Accounts = ?config(accounts, Config), 143 | Test = 144 | [ {204, [admin]}, 145 | {403, Accounts -- [admin]} ], 146 | 147 | [begin 148 | [begin 149 | Pid = idp_cth:gun_open(Config), 150 | Ref = gun:request(Pid, <<"PUT">>, Path, idp_cth:authorization_headers(A, Config)), 151 | {St, _Hs, _Body} = idp_cth:gun_await(Pid, Ref) 152 | end || A <- As] 153 | end || {St, As} <- Test]. 154 | 155 | %% Disables the specified account. 156 | %% The 204 'No content' is returned on success. 157 | %% The 404 'Not Found' error is returned for accounts that don't exist. 158 | delete(Config) -> 159 | #{id := Akey} = ?config(user, Config), 160 | #{id := AkeyDisabled} = ?config(user_disabled, Config), 161 | AkeyNotExist = idp:make_uuid(), 162 | AuthorizationH = idp_cth:authorization_header(admin, Config), 163 | Test = 164 | [ %% account me (admin) 165 | {<<"/api/v1/accounts/me/enabled">>, 204}, 166 | %% account exists 167 | {[<<"/api/v1/accounts/">>, Akey, <<"/enabled">>], 204}, 168 | %% disabled account 169 | {[<<"/api/v1/accounts/">>, AkeyDisabled, <<"/enabled">>], 204}, 170 | %% account doesn't exist 171 | {[<<"/api/v1/accounts/">>, AkeyNotExist, <<"/enabled">>], 404} ], 172 | 173 | [begin 174 | Pid = idp_cth:gun_open(Config), 175 | Ref = gun:request(Pid, <<"DELETE">>, Path, [AuthorizationH]), 176 | {Status, _Hs, <<>>} = idp_cth:gun_await(Pid, Ref) 177 | end || {Path, Status} <- Test]. 178 | 179 | %% Access is granted only for members of 'admin' (predefined) group. 180 | delete_permissions_admin(Config) -> do_delete_permissions(204, [admin], Config). 181 | delete_permissions(Config) -> do_delete_permissions(403, ?config(accounts, Config) -- [admin], Config). 182 | 183 | do_delete_permissions(Status, Accounts, Config) -> 184 | #{id := Akey} = ?config(user, Config), 185 | Path = [<<"/api/v1/accounts/">>, Akey, <<"/enabled">>], 186 | 187 | [begin 188 | Pid = idp_cth:gun_open(Config), 189 | Ref = gun:request(Pid, <<"DELETE">>, Path, idp_cth:authorization_headers(A, Config)), 190 | {Status, _Hs, _Body} = idp_cth:gun_await(Pid, Ref) 191 | end || A <- Accounts]. 192 | -------------------------------------------------------------------------------- /test/auth_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(auth_SUITE). 26 | -include_lib("common_test/include/ct.hrl"). 27 | 28 | -compile(export_all). 29 | 30 | %% ============================================================================= 31 | %% Common Test callbacks 32 | %% ============================================================================= 33 | 34 | all() -> 35 | application:ensure_all_started(idp), 36 | application:ensure_all_started(gun), 37 | [{group, auth}]. 38 | 39 | groups() -> 40 | [{auth, [parallel], ct_helper:all(?MODULE)}]. 41 | 42 | init_per_suite(Config) -> 43 | idp_cth:init_config() ++ Config. 44 | 45 | end_per_suite(Config) -> 46 | Config. 47 | 48 | %% ============================================================================= 49 | %% Tests 50 | %% ============================================================================= 51 | 52 | %% We must not issue any tokens for disabled accounts. 53 | auth_disabled_account(Config) -> 54 | {ok, Pem} = file:read_file(idp:conf_path(<<"keys/example.priv.pem">>)), 55 | {Alg, Priv} = jose_pem:parse_key(Pem), 56 | ClientUid = idp_cth:make_uid(), 57 | I = [<<"oauth2">>, <<"example">>, ClientUid], 58 | Ib = <<"oauth2.example.", ClientUid/binary>>, 59 | ClientToken = do_create_client_token(ClientUid, Alg, Priv), 60 | ContentTypeH = {<<"content-type">>, <<"application/json">>}, 61 | Payload = jsx:encode(#{grant_type => <<"client_credentials">>, client_token => ClientToken}), 62 | AuthorizationH = fun(Token) -> {<<"authorization">>, [<<"Bearer ">>, Token]} end, 63 | 64 | #{access_token := AccessToken, 65 | refresh_token := RefreshToken} = idp_cli_account:create(#{enable => false, identities => [I]}, #{}), 66 | do_wait(), 67 | 68 | Pid = idp_cth:gun_open(Config), 69 | %% Retrieving an access token is forbidden 70 | Ref0 = gun:request(Pid, <<"POST">>, <<"/api/v1/auth/oauth2.example/token">>, [ContentTypeH], Payload), 71 | {400, _Hs0, _Body0} = idp_cth:gun_await(Pid, Ref0), 72 | %% Linking another account is forbidden 73 | Ref1 = gun:request(Pid, <<"POST">>, <<"/api/v1/auth/oauth2.example/link">>, [AuthorizationH(AccessToken), ContentTypeH], Payload), 74 | {400, _Hs1, _Body1} = idp_cth:gun_await(Pid, Ref1), 75 | %% Refreshing an access token is forbidden 76 | Ref2 = gun:request(Pid, <<"POST">>, <<"/api/v1/accounts/me/refresh">>, [AuthorizationH(RefreshToken)]), 77 | {422, _Hs2, _Body2} = idp_cth:gun_await(Pid, Ref2), 78 | %% Revoking an refresh token is forbidden 79 | Ref3 = gun:request(Pid, <<"POST">>, <<"/api/v1/accounts/me/revoke">>, [AuthorizationH(RefreshToken)]), 80 | {422, _Hs3, _Body3} = idp_cth:gun_await(Pid, Ref3), 81 | %% Deleting an account is forbidden 82 | Ref4 = gun:request(Pid, <<"DELETE">>, <<"/api/v1/accounts/me">>, [AuthorizationH(AccessToken)]), 83 | {422, _Hs4, _Body4} = idp_cth:gun_await(Pid, Ref4), 84 | %% Deleting an identity of account is forbidden 85 | Ref5 = gun:request(Pid, <<"DELETE">>, [<<"/api/v1/accounts/me/auth/">>, Ib], [AuthorizationH(AccessToken)]), 86 | {422, _Hs5, _Body5} = idp_cth:gun_await(Pid, Ref5). 87 | 88 | %% Access and refresh tokens can be retrieved by using credentials of an external service (client) 89 | %% in a form of Json Web Token issued and signed by the client. 90 | auth_oauth2_client_credentials(Config) -> 91 | {ok, Pem} = file:read_file(idp:conf_path(<<"keys/example.priv.pem">>)), 92 | {Alg, Priv} = jose_pem:parse_key(Pem), 93 | ClientToken = do_create_client_token(idp_cth:make_uid(), Alg, Priv), 94 | ContentTypeJsonH = {<<"content-type">>, <<"application/json">>}, 95 | ContentTypeFormH = {<<"content-type">>, <<"application/x-www-form-urlencoded">>}, 96 | PayloadJson = jsx:encode(#{grant_type => <<"client_credentials">>, client_token => ClientToken}), 97 | PayloadForm = <<"grant_type=client_credentials&client_token=", ClientToken/binary>>, 98 | Test = 99 | [ {ContentTypeJsonH, PayloadJson}, 100 | {ContentTypeFormH, PayloadForm} ], 101 | 102 | Pid = idp_cth:gun_open(Config), 103 | [begin 104 | Ref = gun:request(Pid, <<"POST">>, <<"/api/v1/auth/oauth2.example/token">>, [ContentTypeH], Payload), 105 | {200, _Hs, #{<<"access_token">> := _, <<"refresh_token">> := _, <<"expires_in">> := _, <<"token_type">> := <<"Bearer">>}} = idp_cth:gun_await_json(Pid, Ref) 106 | end || {ContentTypeH, Payload} <- Test]. 107 | 108 | %% Access token can be refreshed by using previously issued refresh token. 109 | auth_access_token_refresh(Config) -> 110 | {ok, Pem} = file:read_file(idp:conf_path(<<"keys/example.priv.pem">>)), 111 | {Alg, Priv} = jose_pem:parse_key(Pem), 112 | ClientToken = do_create_client_token(idp_cth:make_uid(), Alg, Priv), 113 | ContentTypeH = {<<"content-type">>, <<"application/json">>}, 114 | Payload = jsx:encode(#{grant_type => <<"client_credentials">>, client_token => ClientToken}), 115 | AuthorizationH = fun(Token) -> {<<"authorization">>, [<<"Bearer ">>, Token]} end, 116 | 117 | Pid = idp_cth:gun_open(Config), 118 | %% Getting a refresh token 119 | Ref0 = gun:request(Pid, <<"POST">>, <<"/api/v1/auth/oauth2.example/token">>, [ContentTypeH], Payload), 120 | {200, _Hs0, #{<<"refresh_token">> := RefreshToken}} = idp_cth:gun_await_json(Pid, Ref0), 121 | %% Refreshing access token using a refresh token 122 | Ref1 = gun:request(Pid, <<"POST">>, <<"/api/v1/accounts/me/refresh">>, [AuthorizationH(RefreshToken)]), 123 | {200, _Hs1, #{<<"access_token">> := _, <<"expires_in">> := _, <<"token_type">> := _}} = idp_cth:gun_await_json(Pid, Ref1), 124 | %% Multiple refreshing access token using same refresh token 125 | Ref2 = gun:request(Pid, <<"POST">>, <<"/api/v1/accounts/me/refresh">>, [AuthorizationH(RefreshToken)]), 126 | {200, _Hs2, #{<<"access_token">> := _, <<"expires_in">> := _, <<"token_type">> := _}} = idp_cth:gun_await_json(Pid, Ref2). 127 | 128 | %% Refresh token can be revoked by using previously issued refresh token. 129 | auth_refresh_token_revoke(Config) -> 130 | {ok, Pem} = file:read_file(idp:conf_path(<<"keys/example.priv.pem">>)), 131 | {Alg, Priv} = jose_pem:parse_key(Pem), 132 | ClientToken = do_create_client_token(idp_cth:make_uid(), Alg, Priv), 133 | ContentTypeH = {<<"content-type">>, <<"application/json">>}, 134 | Payload = jsx:encode(#{grant_type => <<"client_credentials">>, client_token => ClientToken}), 135 | AuthorizationH = fun(Token) -> {<<"authorization">>, <<"Bearer ", Token/binary>>} end, 136 | 137 | Pid = idp_cth:gun_open(Config), 138 | %% Getting a refresh token 139 | Ref0 = gun:request(Pid, <<"POST">>, <<"/api/v1/auth/oauth2.example/token">>, [ContentTypeH], Payload), 140 | {200, _Hs0, #{<<"refresh_token">> := RefreshToken}} = idp_cth:gun_await_json(Pid, Ref0), 141 | %% Revoking the refresh token 142 | Ref1 = gun:request(Pid, <<"POST">>, <<"/api/v1/accounts/me/revoke">>, [AuthorizationH(RefreshToken)]), 143 | {200, _Hs1, #{<<"refresh_token">> := _}} = idp_cth:gun_await_json(Pid, Ref1), 144 | %% Confirming that refresh token is revoked 145 | Ref2 = gun:request(Pid, <<"POST">>, <<"/api/v1/accounts/me/revoke">>, [AuthorizationH(RefreshToken)]), 146 | {401, _Hs2, _Body2} = idp_cth:gun_await(Pid, Ref2). 147 | 148 | %% Accounts can have more than one identity linked to it. 149 | auth_link(Config) -> 150 | {ok, Pem} = file:read_file(idp:conf_path(<<"keys/example.priv.pem">>)), 151 | {Alg, Priv} = jose_pem:parse_key(Pem), 152 | ClientAkey = <<"oauth2.example">>, 153 | ClientBkey = <<"oauth2.example-restricted">>, 154 | ClientAuid = idp_cth:make_uid(), 155 | ClientBuid = idp_cth:make_uid(), 156 | ClientAtoken = do_create_client_token(ClientAuid, Alg, Priv), 157 | ClientBtoken = do_create_client_token(ClientBuid, Alg, Priv), 158 | ContentTypeH = {<<"content-type">>, <<"application/json">>}, 159 | ClientApayload = jsx:encode(#{grant_type => <<"client_credentials">>, client_token => ClientAtoken}), 160 | ClientBpayload = jsx:encode(#{grant_type => <<"client_credentials">>, client_token => ClientBtoken}), 161 | IdentityA = <<"oauth2.example.", ClientAuid/binary>>, 162 | Keys = [[<<"oauth2">>, <<"example">>], [<<"oauth2">>, <<"example-restricted">>]], 163 | 164 | Pid = idp_cth:gun_open(Config), 165 | Aref = gun:request(Pid, <<"POST">>, [<<"/api/v1/auth/">>, ClientAkey, <<"/token">>], [ContentTypeH], ClientApayload), 166 | {200, _Ahs, #{<<"access_token">> := Token}} = idp_cth:gun_await_json(Pid, Aref), 167 | [_, #{<<"sub">> := Akey} | _] = jose_jws_compact:parse(Token, #{parse_payload => map}), 168 | 169 | do_wait(), 170 | do_has_account(Akey), 171 | AuthorizationH = {<<"authorization">>, [<<"Bearer ">>, Token]}, 172 | Bref = gun:request(Pid, <<"POST">>, [<<"/api/v1/auth/">>, ClientBkey, <<"/link">>], [AuthorizationH, ContentTypeH], ClientBpayload), 173 | {200, _Bhs, #{<<"id">> := IdentityB}} = idp_cth:gun_await_json(Pid, Bref), 174 | 175 | do_wait(), 176 | do_has_identities(Akey, Keys, [IdentityA, IdentityB]). 177 | 178 | %% ============================================================================= 179 | %% Internal functions 180 | %% ============================================================================= 181 | 182 | -spec do_create_client_token(binary(), binary(), binary()) -> binary(). 183 | do_create_client_token(Uid, Alg, Priv) -> 184 | jose_jws_compact:encode( 185 | #{aud => <<"app.example.org">>, 186 | iss => <<"example.org">>, 187 | exp => 32503680000, 188 | sub => Uid}, 189 | Alg, 190 | Priv). 191 | 192 | -spec do_create_client_token(binary(), binary(), binary(), binary()) -> binary(). 193 | do_create_client_token(Uid, Alg, Priv, Role) -> 194 | jose_jws_compact:encode( 195 | #{aud => <<"app.example.org">>, 196 | iss => <<"example.org">>, 197 | exp => 32503680000, 198 | sub => Uid, 199 | role => Role}, 200 | Alg, 201 | Priv). 202 | 203 | -spec do_create_client_token(binary(), binary(), binary(), binary(), map()) -> binary(). 204 | do_create_client_token(Uid, Alg, Priv, Role, Resource) -> 205 | jose_jws_compact:encode( 206 | #{aud => <<"app.example.org">>, 207 | iss => <<"example.org">>, 208 | exp => 32503680000, 209 | sub => Uid, 210 | role => Role, 211 | resource => Resource}, 212 | Alg, 213 | Priv). 214 | 215 | -spec do_has_account(binary()) -> ok. 216 | do_has_account(Key) -> 217 | #{account := #{bucket := Bucket, pool := KVpool}} = idp:resources(), 218 | KVpid = riakc_pool:lock(KVpool), 219 | _ = riakauth_account:get(KVpid, Bucket, Key), 220 | riakc_pool:unlock(KVpool, KVpid), 221 | ok. 222 | 223 | -spec do_has_identities(binary(), [[binary()]], [binary()]) -> ok. 224 | do_has_identities(Key, Keys, ExpectedIdentities) -> 225 | #{account := #{bucket := Bucket, pool := KVpool}} = idp:resources(), 226 | IdentityToBinary = 227 | fun 228 | ([Segment]) -> Segment; 229 | ([H|T]) -> lists:foldl(fun(Segment, Acc) -> <> end, H, T); 230 | ([]) -> <<>> 231 | end, 232 | Handle = fun(Identity, _Raw, Acc) -> [IdentityToBinary(Identity)|Acc] end, 233 | KVpid = riakc_pool:lock(KVpool), 234 | A = riakauth_account:get(KVpid, Bucket, Key), 235 | riakc_pool:unlock(KVpool, KVpid), 236 | Identities = riakauth_account:fold_identities_dt(Handle, Keys, [], A), 237 | [true = lists:member(ExpectedIdentity, Identities) || ExpectedIdentity <- ExpectedIdentities], 238 | ok. 239 | 240 | -spec do_wait() -> ok. 241 | do_wait() -> 242 | timer:sleep(3000). 243 | -------------------------------------------------------------------------------- /test/idp_cth.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% The MIT License 3 | %% 4 | %% Copyright (c) 2016-2017 Andrei Nesterov 5 | %% 6 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 7 | %% of this software and associated documentation files (the "Software"), to 8 | %% deal in the Software without restriction, including without limitation the 9 | %% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | %% sell copies of the Software, and to permit persons to whom the Software is 11 | %% furnished to do so, subject to the following conditions: 12 | %% 13 | %% The above copyright notice and this permission notice shall be included in 14 | %% all copies or substantial portions of the Software. 15 | %% 16 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | %% IN THE SOFTWARE. 23 | %% ---------------------------------------------------------------------------- 24 | 25 | -module(idp_cth). 26 | 27 | -include_lib("riakc/include/riakc.hrl"). 28 | 29 | %% API 30 | -export([ 31 | init_config/0, 32 | gun_open/1, 33 | gun_await/2, 34 | gun_await_json/2, 35 | gun_down/1, 36 | make_uid/0, 37 | authorization_headers/2, 38 | authorization_header/2 39 | ]). 40 | 41 | %% ============================================================================= 42 | %% API 43 | %% ============================================================================= 44 | 45 | -spec init_config() -> list(). 46 | init_config() -> 47 | []. 48 | 49 | -spec gun_open(list()) -> pid(). 50 | gun_open(_Config) -> 51 | Host = "localhost", 52 | {_, Port} = lists:keyfind(port, 1, idp:http_options()), 53 | {ok, Pid} = gun:open(Host, Port, #{retry => 0, protocols => [http2], transport => ssl}), 54 | Pid. 55 | 56 | -spec gun_down(pid()) -> ok. 57 | gun_down(Pid) -> 58 | receive {gun_down, Pid, _, _, _, _} -> ok 59 | after 500 -> error(timeout) end. 60 | 61 | -spec gun_await(pid(), reference()) -> {100..999, [{binary(), iodata()}], binary()}. 62 | gun_await(Pid, Ref) -> 63 | case gun:await(Pid, Ref) of 64 | {response, fin, St, Hs} -> {St, Hs, <<>>}; 65 | {response, nofin, St, Hs} -> 66 | {ok, Body} = gun:await_body(Pid, Ref), 67 | {St, Hs, Body} 68 | end. 69 | 70 | -spec gun_await_json(pid(), reference()) -> {100..999, [{binary(), iodata()}], map()}. 71 | gun_await_json(Pid, Ref) -> 72 | {St, Hs, Body} = gun_await(Pid, Ref), 73 | try {St, Hs, jsx:decode(Body, [return_maps, strict])} 74 | catch _:_ -> error({bad_response, {St, Hs, Body}}) end. 75 | 76 | -spec make_uid() -> iodata(). 77 | make_uid() -> 78 | list_to_binary(vector(8, alphanum_chars())). 79 | 80 | -spec authorization_headers(atom(), list()) -> [{binary(), iodata()}]. 81 | authorization_headers(anonymous, _Config) -> []; 82 | authorization_headers(Account, Config) -> [authorization_header(Account, Config)]. 83 | 84 | -spec authorization_header(atom(), list()) -> {binary(), iodata()}. 85 | authorization_header(Account, Config) -> 86 | {_, #{access_token := Token}} = lists:keyfind(Account, 1, Config), 87 | {<<"authorization">>, [<<"Bearer ">>, Token]}. 88 | 89 | %%% ============================================================================= 90 | %%% Internal functions 91 | %%% ============================================================================= 92 | 93 | -spec oneof(list()) -> integer(). 94 | oneof(L) -> 95 | lists:nth(rand:uniform(length(L)), L). 96 | 97 | -spec vector(non_neg_integer(), list()) -> list(). 98 | vector(MaxSize, L) -> 99 | vector(0, MaxSize, L, []). 100 | 101 | -spec vector(non_neg_integer(), non_neg_integer(), list(), list()) -> list(). 102 | vector(Size, MaxSize, L, Acc) when Size < MaxSize -> 103 | vector(Size +1, MaxSize, L, [oneof(L)|Acc]); 104 | vector(_, _, _, Acc) -> 105 | Acc. 106 | 107 | -spec alphanum_chars() -> list(). 108 | alphanum_chars() -> 109 | "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ". 110 | --------------------------------------------------------------------------------