├── .editorconfig
├── .github
└── demo.png
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── TROUBLESHOOTING.md
├── deploy.sh
├── deploy_examples.sh
├── elm.json
├── examples
├── Makefile
├── README.md
├── assets
│ └── css
│ │ ├── branding.css
│ │ └── style.css
├── dist
│ └── .gitkeep
├── index.html
└── providers
│ ├── auth0
│ ├── README.md
│ ├── authorization-code
│ │ ├── Main.elm
│ │ ├── README.md
│ │ ├── elm.json
│ │ └── src
│ ├── implicit
│ │ ├── Main.elm
│ │ ├── README.md
│ │ ├── elm.json
│ │ └── src
│ └── pkce
│ │ ├── Main.elm
│ │ ├── README.md
│ │ ├── elm.json
│ │ └── src
│ ├── facebook
│ ├── README.md
│ └── implicit
│ │ ├── Main.elm
│ │ ├── elm.json
│ │ └── src
│ ├── google
│ ├── README.md
│ └── implicit
│ │ ├── Main.elm
│ │ ├── README.md
│ │ ├── elm.json
│ │ └── src
│ └── spotify
│ ├── README.md
│ └── implicit
│ ├── Main.elm
│ ├── README.md
│ ├── elm.json
│ └── src
├── src
├── Extra
│ └── Maybe.elm
├── Internal.elm
├── OAuth.elm
└── OAuth
│ ├── AuthorizationCode.elm
│ ├── AuthorizationCode
│ └── PKCE.elm
│ ├── ClientCredentials.elm
│ ├── Implicit.elm
│ ├── Password.elm
│ └── Refresh.elm
└── tests
└── Test
└── Parsers.elm
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 |
9 | [Makefile]
10 | indent_style = tab
11 | indent_size = 8
12 |
13 | [*.elm]
14 | indent_size = 4
15 | indent_style = space
16 | max_line_length = 100
17 |
18 | [*.js]
19 | indent_size = 4
20 | indent_style = space
21 | max_line_length = 100
22 |
--------------------------------------------------------------------------------
/.github/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/truqu/elm-oauth2/ef6a7bf29b361a2564b99b0daa79eb3b7ed74f45/.github/demo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### elm ###
2 | elm-stuff
3 | repl-temp-*
4 |
5 |
6 | ### tmux ###
7 | *.log
8 |
9 |
10 | ### others / builds ###
11 | *.min.js
12 | *.html
13 | !examples/**/index.html
14 | examples/index.html
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v8.0.1 (2021-08-08)
2 |
3 | - Softly deprecate Implicit and put AuthorizationCode and AuthorizationCode w/ PKCE more in the spotlights, as per security recommendations.
4 | - Documentation tweaks and improvements (better cross-link references, better code highlights, links to examples).
5 |
6 | ## v8.0.0 (2021-06-30)
7 |
8 | - Allow more advanced control for tweaking parsers, decoders and url builders. This is particularly useful for applications integrating with systems which are either not strictly following the OAuth2.0 specifications, or, systems who introduce custom fields of some importance for the underlying application. (see #29, #23, #21)
9 |
10 | - Update dependencies for base64 encoding
11 |
12 | ### Diff
13 |
14 | #### `OAuth` - MINOR
15 |
16 | - Added:
17 |
18 | ```elm
19 | type GrantType
20 | = AuthorizationCode
21 | | Password
22 | | ClientCredentials
23 | | RefreshToken
24 | | CustomGrant String
25 |
26 | grantTypeToString : GrantType -> String
27 | ```
28 |
29 | ```elm
30 | type ResponseType
31 | = Code
32 | | Token
33 | | CustomResponse String
34 |
35 | responseTypeToString : ResponseType -> String
36 | ```
37 |
38 | #### `OAuth.Implicit` - MAJOR
39 |
40 | - Added:
41 |
42 | ```elm
43 | makeAuthorizationUrlWith :
44 | ResponseType
45 | -> Dict String String
46 | -> Authorization
47 | -> Url
48 | ```
49 |
50 | - Changed:
51 |
52 | ```elm
53 | -- type alias Parsers =
54 | -- { tokenParser :
55 | -- Query.Parser (Maybe Token)
56 | -- , errorParser :
57 | -- Query.Parser (Maybe ErrorCode)
58 | -- , authorizationSuccessParser :
59 | -- String -> Query.Parser AuthorizationSuccess
60 | -- , authorizationErrorParser :
61 | -- ErrorCode -> Query.Parser AuthorizationError
62 | -- }
63 |
64 | type alias Parsers error success =
65 | { tokenParser :
66 | Query.Parser (Maybe Token)
67 | , errorParser :
68 | Query.Parser (Maybe ErrorCode)
69 | , authorizationSuccessParser :
70 | String -> Query.Parser success
71 | , authorizationErrorParser :
72 | ErrorCode -> Query.Parser error
73 | }
74 | ```
75 |
76 | ```elm
77 | -- defaultParsers : Parsers
78 | defaultParsers : Parsers AuthorizationError AuthorizationSuccess
79 | ```
80 |
81 | ```elm
82 | -- parseTokenWith : Parsers -> Url -> AuthorizationResult
83 | parseTokenWith : Parsers error success -> Url -> AuthorizationResultWith error success
84 | ```
85 |
86 | #### `OAuth.AuthorizationCode` - MAJOR
87 |
88 | - Added:
89 |
90 | ```elm
91 | makeAuthorizationUrlWith :
92 | ResponseType
93 | -> Dict String String
94 | -> Authorization
95 | -> Url
96 | ```
97 |
98 | ```elm
99 | makeTokenRequestWith :
100 | OAuth.GrantType
101 | -> Json.Decoder success
102 | -> Dict String String
103 | -> (Result Http.Error success -> msg)
104 | -> Authentication
105 | -> RequestParts msg
106 | ```
107 |
108 | - Changed:
109 |
110 | ```elm
111 | -- type AuthorizationResult
112 | -- = Empty
113 | -- | Error AuthorizationError
114 | -- | Success AuthorizationSuccess
115 |
116 | type alias AuthorizationResult =
117 | AuthorizationResultWith AuthorizationError AuthorizationSuccess
118 |
119 | type AuthorizationResultWith error success
120 | = Empty
121 | | Error error
122 | | Success success
123 | ```
124 |
125 | ```elm
126 | -- type alias Parsers =
127 | -- { codeParser :
128 | -- Query.Parser (Maybe String)
129 | -- , errorParser :
130 | -- Query.Parser (Maybe ErrorCode)
131 | -- , authorizationSuccessParser :
132 | -- String -> Query.Parser AuthorizationSuccess
133 | -- , authorizationErrorParser :
134 | -- ErrorCode -> Query.Parser AuthorizationError
135 | -- }
136 |
137 | type alias Parsers error success =
138 | { codeParser :
139 | Query.Parser (Maybe String)
140 | , errorParser :
141 | Query.Parser (Maybe ErrorCode)
142 | , authorizationSuccessParser :
143 | String -> Query.Parser success
144 | , authorizationErrorParser :
145 | ErrorCode -> Query.Parser error
146 | }
147 | ```
148 |
149 | ```elm
150 | -- defaultParsers : Parsers
151 | defaultParsers : Parsers AuthorizationError AuthorizationSuccess
152 | ```
153 |
154 | ```elm
155 | -- parseCodeWith : Parsers -> Url -> AuthorizationResult
156 | parseCodeWith : Parsers error success -> Url -> AuthorizationResultWith error success
157 | ```
158 |
159 | #### `OAuth.AuthorizationCode.PKCE` - MAJOR
160 |
161 | - Added:
162 |
163 | ```elm
164 | makeAuthorizationUrlWith :
165 | ResponseType
166 | -> Dict String String
167 | -> Authorization
168 | -> Url
169 | ```
170 |
171 | ```elm
172 | makeTokenRequestWith :
173 | OAuth.GrantType
174 | -> Json.Decoder success
175 | -> Dict String String
176 | -> (Result Http.Error success -> msg)
177 | -> Authentication
178 | -> RequestParts msg
179 | ```
180 |
181 | - Changed:
182 |
183 | ```elm
184 | -- type AuthorizationResult
185 | -- = Empty
186 | -- | Error AuthorizationError
187 | -- | Success AuthorizationSuccess
188 |
189 | type alias AuthorizationResult =
190 | AuthorizationResultWith AuthorizationError AuthorizationSuccess
191 |
192 | type AuthorizationResultWith error success
193 | = Empty
194 | | Error error
195 | | Success success
196 | ```
197 |
198 | ```elm
199 | -- type alias Parsers =
200 | -- { codeParser :
201 | -- Query.Parser (Maybe String)
202 | -- , errorParser :
203 | -- Query.Parser (Maybe ErrorCode)
204 | -- , authorizationSuccessParser :
205 | -- String -> Query.Parser AuthorizationSuccess
206 | -- , authorizationErrorParser :
207 | -- ErrorCode -> Query.Parser AuthorizationError
208 | -- }
209 |
210 | type alias Parsers error success =
211 | { codeParser :
212 | Query.Parser (Maybe String)
213 | , errorParser :
214 | Query.Parser (Maybe ErrorCode)
215 | , authorizationSuccessParser :
216 | String -> Query.Parser success
217 | , authorizationErrorParser :
218 | ErrorCode -> Query.Parser error
219 | }
220 | ```
221 |
222 | ```elm
223 | -- defaultParsers : Parsers
224 | defaultParsers : Parsers AuthorizationError AuthorizationSuccess
225 | ```
226 |
227 | ```elm
228 | -- parseCodeWith : Parsers -> Url -> AuthorizationResult
229 | parseCodeWith : Parsers error success -> Url -> AuthorizationResultWith error success
230 | ```
231 |
232 |
233 | #### `OAuth.ClientCredentials` - MINOR
234 |
235 | - Added:
236 |
237 | ```elm
238 | makeTokenRequestWith :
239 | GrantType
240 | -> Json.Decoder success
241 | -> Dict String String
242 | -> (Result Http.Error success -> msg)
243 | -> Authentication
244 | -> RequestParts msg
245 | ```
246 |
247 | #### `OAuth.Password` - MINOR
248 |
249 | - Added:
250 |
251 | ```elm
252 | makeTokenRequestWith :
253 | GrantType
254 | -> Json.Decoder success
255 | -> Dict String String
256 | -> (Result Http.Error success -> msg)
257 | -> Authentication
258 | -> RequestParts msg
259 | ```
260 |
261 |
262 | #### `OAuth.Refresh` - MINOR
263 |
264 | - Added:
265 |
266 | ```elm
267 | makeTokenRequestWith :
268 | GrantType
269 | -> Json.Decoder success
270 | -> Dict String String
271 | -> (Result Http.Error success -> msg)
272 | -> Authentication
273 | -> RequestParts msg
274 | ```
275 |
276 | ## v7.0.1 (2020-12-05)
277 |
278 | - Updated dependency `ivadzy/bbase64@1.1.1` renamed as `chelovek0v/bbase64@1.0.1`
279 |
280 | ## v7.0.0 (2020-02-17)
281 |
282 | #### Diff
283 |
284 | ```elm
285 | ---- ADDED MODULES - MINOR ----
286 |
287 | OAuth.AuthorizationCode.PKCE
288 |
289 |
290 | ---- OAuth.AuthorizationCode - MAJOR ----
291 |
292 | Added:
293 | type alias AuthorizationCode = String.String
294 |
295 | Changed:
296 | - type alias AuthorizationSuccess =
297 | { code : String, state : Maybe String }
298 | + type alias AuthorizationSuccess =
299 | { code : OAuth.AuthorizationCode.AuthorizationCode
300 | , state : Maybe.Maybe String.String
301 | }
302 | ```
303 |
304 | #### Commits
305 |
306 | - f1f648a76fcc0e8e33ef06cd9867600164d709d7 add support for RFC7636 - Proof Key for Code Exchange
307 |
308 | Auth 2.0 public clients utilizing the Authorization Code Grant are
309 | susceptible to the authorization code interception attack. This
310 | specification describes the attack as well as a technique to mitigate against
311 | the threat through the use of Proof Key for Code Exchange (PKCE, pronounced
312 | "pixy").
313 |
314 | - 3dc3c9d6a0aa6d20b84d8ffc79e55aec06beb683 remove double dependency on base64 and favor only one
315 |
316 | - 6199c78126d59fe0da5ed491f04835087285188a several doc revision on all grants (diagrams, type description etc ...)
317 |
318 | - 0d969a08dd90079933f747c24cea8c13b9954a07 put PKCE as recommended in README and start reviewing demos / guides
319 |
320 | - b712fcdec341bb3b07a95fbcf5e77c6794f7da01 rework examples
321 | - Add auth0 example with authorization code and PKCE support
322 | - Add facebook example
323 | - Make them more readable and avoid unrelated code in examples
324 | - Add README to summarize information
325 |
326 | - 68383cfa0d22c29733a219a2849db3cfc2731e63 revise deployment scripts, in particular examples
327 |
328 | - f86ffe9469f50b9c011505459df380fe071b604c bump version (major) to 7.0.0 & update CHANGELOG
329 |
330 |
331 | ## v6.0.0 (2019-09-03)
332 |
333 | - (267ca48) Internal small refactor
334 | - (43e536a) General documentation improvements
335 | - (e34e16f) Rename 'makeAuthUrl' to 'makeAuthorizationUrl'
336 | - (12ce2ba) Split-up README, extract troubleshooting and guides
337 |
338 | ## v5.0.0 (2019-01-23)
339 |
340 | - (d74016e, 333d6ea, 849d985, 78caba7) Upgrade `elm/http` to new major version `2.0.0`
341 |
342 |
343 | ## v4.0.1 (2018-10-06)
344 |
345 | - (15e4e82) Bug Fix: make token\_type parsing case-insensitive.
346 |
347 |
348 | ## v4.0.0 (2018-09-07)
349 |
350 | - (72f251a, 1327646) Documentation improvements
351 |
352 | - (0105ca3, 9a3b307, 5e3c841, 4801593) Review examples to be more complete, self-explanatory and clearer
353 |
354 | - (0ac7d90) Completely review internal implementation & exposed API
355 |
356 |
357 | ## v3.0.0 (2018-09-03)
358 |
359 | - (3a60354) Upgrade `src/` to `elm@0.19`
360 | - (ef85924) Upgrade `examples/implicit` to `elm@0.19`
361 | - (88f27a7) Remove `examples/authorization_code`
362 | - (7ce7c82) Change `String` to `Url` for
363 | - `Authorization.url`
364 | - `Authorization.redirectUri`
365 | - `Authentication#AuthorizationCode.redirectUri`
366 | - `Authentication#AuthorizationCode.url`
367 | - `Authentication#ClientCredentials.url`
368 | - `Authentication#Password.url`
369 | - `Authentication#Refresh.url`
370 | - (912197c) Expose `lenientResponseDecoder` from `OAuth.Decode`
371 |
372 |
373 | ## v2.2.1 (2018-08-16)
374 |
375 | - Bump `elm-base64` version upper-bound
376 |
377 |
378 | ## v2.2.0 (2017-12-22)
379 |
380 | - (oversight) Actually expose 'authenticateWithOpts' functions from modules
381 |
382 |
383 | ## v2.1.0 (2017-12-22)
384 |
385 | - Expose internal Json decoders
386 | - Enable users to adjust requests made to the Authorization Server to cope with possible
387 | implementation quirks (like GitHub API v3)
388 |
389 |
390 | ## v2.0.3 (2017-06-04)
391 |
392 | - Update LICENSE's information
393 | - Fix broken links and examples in README
394 |
395 |
396 | ## v2.0.2 (2017-06-02)
397 |
398 | - Fix bug about empty scope parameter being sent when `Nothing` is provided as a scope
399 |
400 |
401 | ## v2.0.1 (2017-06-02)
402 |
403 | - Enhance documentation about response parameters
404 |
405 | ## v2.0.0 (2017-06-02)
406 |
407 | - Review type `Response` to provide a clearer API
408 | - Fix typos and references in examples
409 |
410 |
411 | ## v1.0.0 (2017-06-01)
412 |
413 | - Initial release, support for all 4 grant types.
414 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-2019 TruQu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included
14 | in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Elm OAuth 2 [](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest)
2 | =====
3 |
4 | This package offers some utilities to implement a client-side [OAuth 2](https://tools.ietf.org/html/rfc6749) authorization in Elm. It covers all four basic grant types as well as the [PKCE](https://tools.ietf.org/html/rfc7636) extension:
5 |
6 | - **(RECOMMENDED)** [Authorization Code w/ PKCE](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest/OAuth-AuthorizationCode-PKCE):
7 | An extension of the original OAuth 2.0 specification to mitigate authorization code interception attacks through the use of Proof Key for Code Exchange (PKCE). **FOR PUBLIC & CONFIDENTIAL CLIENTS** such as the device operating system or a highly privileged application that has been issued credentials for authenticating with the authorization server (e.g. a client id).
8 |
9 | - [Authorization Code](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest/OAuth-AuthorizationCode):
10 | The token is obtained as a result of an authentication, from a code obtained as a result of a user redirection to an OAuth provider. The authorization code grant type is used to obtain both access tokens and refresh tokens and is optimized **FOR PUBLIC & CONFIDENTIAL CLIENTS** such as the device operating system or a highly privileged application that has been issued credentials for authenticating with the authorization server (e.g. a client id).
11 |
12 | - [Client Credentials](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest/OAuth-ClientCredentials):
13 | The token is obtained directly by exchanging application credentials with an OAuth provider. The client credentials grant type must only be **USED BY CONFIDENTIAL CLIENTS**.
14 |
15 | - [Resource Owner Password Credentials](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest/OAuth-Password):
16 | The token is obtained directly by exchanging the user credentials with an OAuth provider. The resource owner password credentials grant type is suitable in cases **WHERE THE RESOURCE OWNER HAS A TRUST RELATIONSHIP WITH THE CLIENT**.
17 |
18 | - **(DEPRECATED)\*** [Implicit](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest/OAuth-Implicit):
19 | The token is obtained directly as a result of a user redirection to an OAuth provider. The implicit grant type is used to obtain access tokens (it does not support the issuance of refresh tokens) and is optimized **FOR PUBLIC CLIENTS**.
20 |
21 | (\*) https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-16#section-2.1.2
22 |
23 |
24 |
25 | ## Getting Started
26 |
27 | ### Installation
28 |
29 | ```
30 | elm install truqu/elm-oauth2
31 | ```
32 |
33 | ### Examples / Demos
34 |
35 | - [Auth0](https://github.com/truqu/elm-oauth2/tree/master/examples/providers/auth0)
36 | - [Facebook](https://github.com/truqu/elm-oauth2/tree/master/examples/providers/facebook)
37 | - [Google](https://github.com/truqu/elm-oauth2/tree/master/examples/providers/google)
38 | - [Spotify](https://github.com/truqu/elm-oauth2/tree/master/examples/providers/spotify)
39 |
40 | ### Troubleshooting
41 |
42 | [TROUBLESHOOTING.md](https://github.com/truqu/elm-oauth2/tree/master/TROUBLESHOOTING.md)
43 |
44 | ## Changelog
45 |
46 | [CHANGELOG.md](https://github.com/truqu/elm-oauth2/tree/master/CHANGELOG.md)
47 |
--------------------------------------------------------------------------------
/TROUBLESHOOTING.md:
--------------------------------------------------------------------------------
1 |
2 | Understanding OAuth roles
3 |
4 | Throughout the library, you'll find terms referring to OAuth well-defined roles:
5 |
6 | - **`resource owner`**
7 | _An entity capable of granting access to a protected resource.
8 | When the resource owner is a person, it is referred to as an
9 | end-user._
10 |
11 | - **`client`**
12 | _An application making protected resource requests on behalf of the
13 | resource owner and with its authorization. The term "client" does
14 | not imply any particular implementation characteristics (e.g.,
15 | whether the application executes on a server, a desktop, or other
16 | devices)._
17 |
18 | - **`authorization server`**
19 | _The server issuing access tokens to the client after successfully
20 | authenticating the resource owner and obtaining authorization._
21 |
22 | - **`resource server`**
23 | _The server hosting the protected resources, capable of accepting
24 | and responding to protected resource requests using access tokens._
25 |
26 | > NOTE: Usually, the _authorization server_ and the _resource server_ are
27 | > a same entity, or comes from the same entity. So, a simplified vision of
28 | > this roles can be:
29 | >
30 | > - **`resource owner`**
31 | > The end-user
32 | >
33 | > - **`client`**
34 | > Your Elm app
35 | >
36 | > - **`authorization server`** / **`resource server`**
37 | > Google, Facebook, Twitter or whatever OAuth provider you're talking to
38 |
39 |
40 |
41 | Authentication requests in the Authorization Flow don't go through
42 |
43 | Most authorization servers don't enable CORS on the authentication endpoints. For this reason,
44 | it's likely that the preflight _OPTIONS_ requests sent by the browser return an invalid
45 | answer, preventing the browser from making the request at all.
46 |
47 | Why is it so? The authorization request _usually_requires one's secret; thus making them
48 | rather impractical to perform from a client-side application without exposing those secrets.
49 | As a security measure, most authorization servers choose to enforce that those requests are
50 | made server-side instead.
51 |
52 | Generally, this is also what you want, unless you're dealing with a custom authorization server
53 | in some sort of isolated environment. OAuth 2.0 is designed to cover all sort of delegation of
54 | permissions, the case of user-facing client-side applications is only one of them; some
55 | authorization flows are therefore not necessarily adapted to these cases. Usually, a client-side
56 | application will prefer the _Implicit Flow_ over the others.
57 |
58 |
59 | ---
60 |
61 | > Still having an issue?
62 | >
63 | > Please [open a ticket](https://github.com/truqu/elm-oauth2/issues/new) and let us know :heart:!
64 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | function untag () {
4 | git tag -d $1
5 | git push origin --delete $1
6 | }
7 |
8 | ## Verify nothing is unstaged or untracked
9 | status=$(git status -s)
10 | if [ -n "$status" ]; then
11 | echo "branch isn't clean: commit staged files and / or discard untracked files!"
12 | echo $status
13 | exit 1
14 | fi
15 |
16 | ## Verify code compiles
17 | echo "compiling library" && elm make || exit 1
18 | for d in $(ls -d examples/providers/**/** | grep -v README) ; do
19 | cd $d
20 | mkdir -p dist
21 | echo "compiling $d" && elm make --optimize "Main.elm" --output="dist/app.min.js" || exit 1
22 | cd -
23 | done
24 | rm -f index.html
25 |
26 | ## Get version number
27 | version=$(cat elm.json | grep '"version"' | sed 's/\([^0-9]*\)\([0-9]\.[0-9]\.[0-9]\)\(.*\)/\2/')
28 | if [ -z "$version" ]; then
29 | echo "unable to capture package version"
30 | exit 1
31 | else
32 | echo "VERSION: $version"
33 | fi
34 |
35 | ## Create tag and publish
36 | trap 'untag $version' 1
37 | git tag -d $version 1>/dev/null 2>&1
38 | git tag -a $version -m "release version $version" && git push origin $version
39 | elm publish || exit 1
40 |
41 |
42 | ## Deploy examples
43 | source ./deploy_examples.sh
44 | deploy_examples $version
45 |
--------------------------------------------------------------------------------
/deploy_examples.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | function deploy_examples () {
4 | version=$1
5 |
6 | git branch -D gh-pages-$version
7 | git checkout --orphan gh-pages-$version
8 | git reset
9 |
10 | cd examples/providers
11 | for d in $(ls -d **/** | grep -v README); do
12 | mkdir -p ../../$d
13 | cp -r ../index.html ../assets $d/dist ../../$d
14 | git add -f ../../$d/assets
15 | git add -f ../../$d/dist/app.min.js
16 | git add -f ../../$d/index.html
17 | done
18 | cd -
19 | git commit -m "$version"
20 | git branch -M gh-pages && git push origin -f HEAD
21 | git checkout -f master
22 | }
23 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "package",
3 | "name": "truqu/elm-oauth2",
4 | "summary": "OAuth 2.0 client-side utils",
5 | "license": "MIT",
6 | "version": "8.0.1",
7 | "exposed-modules": [
8 | "OAuth",
9 | "OAuth.AuthorizationCode",
10 | "OAuth.AuthorizationCode.PKCE",
11 | "OAuth.Implicit",
12 | "OAuth.ClientCredentials",
13 | "OAuth.Password",
14 | "OAuth.Refresh"
15 | ],
16 | "elm-version": "0.19.0 <= v < 0.20.0",
17 | "dependencies": {
18 | "chelovek0v/bbase64": "1.0.1 <= v < 2.0.0",
19 | "elm/bytes": "1.0.8 <= v < 2.0.0",
20 | "elm/core": "1.0.2 <= v < 2.0.0",
21 | "elm/http": "2.0.0 <= v < 3.0.0",
22 | "elm/json": "1.1.2 <= v < 2.0.0",
23 | "elm/url": "1.0.0 <= v < 2.0.0",
24 | "folkertdev/elm-sha2": "1.0.0 <= v < 2.0.0"
25 | },
26 | "test-dependencies": {
27 | "elm-explorations/test": "1.2.2 <= v < 2.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/Makefile:
--------------------------------------------------------------------------------
1 | ELM=elm make --optimize
2 | DIST=dist
3 | SRC=providers
4 | OUTPUT=../../../$(DIST)/app.min.js
5 |
6 | .PHONY: help start
7 |
8 | help:
9 | @echo -n "Usage: make /"
10 | @echo -n "\n\nAvailable targets:\n"
11 | $(eval DIRS := $(shell find providers -maxdepth 2 -mindepth 2 -type d))
12 | @echo -n $(foreach EL,$(DIRS),$(shell echo "$(EL)" | cut -c 11-)) | tr " " "\n" | sed "s/^/ /"
13 | @echo -n "\n\nExamples:\n make facebook/implicit\n make auth0/pkce"
14 |
15 | start:
16 | python -m SimpleHTTPServer
17 |
18 | %/implicit: $(DIST)
19 | cd $(SRC)/$@ && $(ELM) --output=$(OUTPUT) *.elm
20 |
21 | %/authorization-code: $(DIST)
22 | cd $(SRC)/$@ && $(ELM) --output=$(OUTPUT) *.elm
23 |
24 | %/pkce: $(DIST)
25 | cd $(SRC)/$@ && $(ELM) --output=$(OUTPUT) *.elm
26 |
27 | $(DIST):
28 | mkdir -p $@
29 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | ## Pre-Requisite
4 |
5 | :snake: Python 2+ | :hammer: Make 3+ | :curly_loop: Elm 0.19
6 |
7 | ## Building
8 |
9 | General command:
10 |
11 | ```console
12 | $ make {provider}/{flow}
13 | ```
14 |
15 | Concrete examples:
16 |
17 | ```console
18 | $ make google/implicit
19 | cd providers/google/implicit && elm make --optimize --output=../../../dist/app.min.js *.elm
20 | Success!
21 |
22 | Main ───> ../../../dist/app.min.js
23 |
24 | $ make auth0/authorization-code
25 | cd providers/auth0/authorization-code && elm make --optimize --output=../../../dist/app.min.js *.elm
26 | Success!
27 |
28 | Main ───> ../../../dist/app.min.js
29 | ```
30 |
31 | ## Running
32 |
33 | ```console
34 | $ make start
35 | python -m SimpleHTTPServer
36 | Serving HTTP on 0.0.0.0 port 8000 ...
37 | ```
38 |
39 | Then, visit http://localhost:8000/
40 |
--------------------------------------------------------------------------------
/examples/assets/css/style.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | padding: 0;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | body {
9 | font-family: Roboto, Arial, sans-serif;
10 | width: 100%;
11 | }
12 |
13 | button {
14 | border: none;
15 | box-shadow: rgba(0,0,0,0.25) 0px 2px 4px 0px;
16 | color: #757575;
17 | cursor: pointer;
18 | font-size: 24px;
19 | height: 3em;
20 | margin: 0.5em 0;
21 | outline: none;
22 | padding: 0 1em 0 4em;
23 | text-align: right;
24 | width: 10em;
25 | }
26 |
27 | button::-moz-focus-inner {
28 | border: 0;
29 | }
30 |
31 | img.avatar {
32 | height: 15em;
33 | width: 15em;
34 | border-radius: 50%;
35 | box-shadow: rgba(0,0,0,0.25) 0 0 4px 2px;
36 | }
37 |
38 | img.avatar + p {
39 | margin: 2em;
40 | font: 24px Roboto, Arial;
41 | color: #757575;
42 | }
43 |
44 | .flex {
45 | align-items: center;
46 | display: flex;
47 | justify-content: center;
48 | width: 100%;
49 | }
50 |
51 | .flex-column {
52 | flex-direction: column;
53 | justify-content: center;
54 | }
55 |
56 | .flex-space-around {
57 | height: 100%;
58 | justify-content: space-around;
59 | }
60 |
61 |
62 | .step {
63 | width: 2em;
64 | height: 2em;
65 | border: 1px solid;
66 | border-radius: 50%;
67 | border-color: #95a5a6;
68 | }
69 |
70 | .step > * {
71 | position: relative;
72 | display: block;
73 | top: 2.5em;
74 | color: #95a5a6;
75 | white-space: nowrap;
76 | font-variant: small-caps;
77 | }
78 |
79 | .step-separator {
80 | width: 7.5em;
81 | height: 0.1em;
82 | background-color: #95a5a6;
83 | }
84 |
85 | .step-active {
86 | border-color: #2ecc71;
87 | background-color: #2ecc71;
88 | transition: all 250ms ease-in;
89 | }
90 |
91 | .step-active > * {
92 | font-weight: bold;
93 | color: #2ecc71;
94 | }
95 |
96 | .step-errored {
97 | border-color: #e74c3c;
98 | background-color: #e74c3c;
99 | transition: all 250ms ease-in;
100 | }
101 |
102 | .step-errored > * {
103 | font-weight: bold;
104 | color: #e74c3c;;
105 | }
106 |
107 | .step-separator.step-active {
108 | height: 0.2em;
109 | transition: all 250ms ease-in;
110 | }
111 |
--------------------------------------------------------------------------------
/examples/dist/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/truqu/elm-oauth2/ef6a7bf29b361a2564b99b0daa79eb3b7ed74f45/examples/dist/.gitkeep
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/providers/auth0/README.md:
--------------------------------------------------------------------------------
1 | #
Auth0
2 |
3 | ## Authorization Flows
4 |
5 | Flow | Support | Remark | Example
6 | --- | --- | --- | ---
7 | Implicit | :heavy_check_mark: | \- | [live demo][implicit-demo] \| [source code][implicit-source]
8 | Authorization Code | :heavy_check_mark: | \- | [live demo][authorization-demo] \| [source code][authorization-source]
9 | Authorization Code w/ PKCE | :heavy_check_mark: | \- | [live demo][pkce-demo] \| [source code][pkce-source]
10 | Password | :heavy_check_mark: | \- | N/A
11 | Client Credentials | :heavy_check_mark: | Requires secret key (server-side) | N/A
12 |
13 | ## OAuth Configuration
14 |
15 | \- | \-
16 | --- | ---
17 | Authorization Endpoint | my-app.domain.auth0.com/authorize
18 | Token Endpoint | my-app.domain.auth0.com/oauth/token
19 | User Info Endpoint | my-app.domain.auth0.com/userinfo
20 |
21 | ---
22 |
23 | :book: https://auth0.com/docs/getting-started/overview
24 |
25 |
26 | [implicit-demo]: https://truqu.github.io/elm-oauth2/auth0/implicit/
27 | [implicit-source]: https://github.com/truqu/elm-oauth2/blob/master/examples/providers/auth0/implicit/Main.elm
28 |
29 | [authorization-demo]: https://truqu.github.io/elm-oauth2/auth0/authorization-code/
30 | [authorization-source]: https://github.com/truqu/elm-oauth2/blob/master/examples/providers/auth0/authorization-code/Main.elm
31 |
32 | [pkce-demo]: https://truqu.github.io/elm-oauth2/auth0/pkce/
33 | [pkce-source]: https://github.com/truqu/elm-oauth2/blob/master/examples/providers/auth0/pkce/Main.elm
34 |
--------------------------------------------------------------------------------
/examples/providers/auth0/authorization-code/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing (main)
2 |
3 | import Base64.Encode as Base64
4 | import Browser exposing (Document, application)
5 | import Browser.Navigation as Navigation exposing (Key)
6 | import Bytes exposing (Bytes)
7 | import Bytes.Encode as Bytes
8 | import Delay exposing (TimeUnit(..), after)
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 | import Html.Events exposing (..)
12 | import Http
13 | import Json.Decode as Json
14 | import OAuth
15 | import OAuth.AuthorizationCode as OAuth
16 | import Url exposing (Protocol(..), Url)
17 |
18 |
19 | main : Program (Maybe (List Int)) Model Msg
20 | main =
21 | application
22 | { init =
23 | Maybe.map convertBytes >> init
24 | , update =
25 | update
26 | , subscriptions =
27 | always <| randomBytes GotRandomBytes
28 | , onUrlRequest =
29 | always NoOp
30 | , onUrlChange =
31 | always NoOp
32 | , view =
33 | view
34 | { title = "Auth0 - Flow: Authorization Code"
35 | , btnClass = class "btn-auth0"
36 | }
37 | }
38 |
39 |
40 | {-| OAuth configuration.
41 |
42 | Note that this demo also fetches basic user information with the obtained access token,
43 | hence the user info endpoint and JSON decoder
44 |
45 | -}
46 | configuration : Configuration
47 | configuration =
48 | { authorizationEndpoint =
49 | { defaultHttpsUrl | host = "elm-oauth2.eu.auth0.com", path = "/authorize" }
50 | , tokenEndpoint =
51 | { defaultHttpsUrl | host = "elm-oauth2.eu.auth0.com", path = "/oauth/token" }
52 | , userInfoEndpoint =
53 | { defaultHttpsUrl | host = "elm-oauth2.eu.auth0.com", path = "/userinfo" }
54 | , userInfoDecoder =
55 | Json.map2 UserInfo
56 | (Json.field "name" Json.string)
57 | (Json.field "picture" Json.string)
58 | , clientId =
59 | "AbRrXEIRBPgkDrqR4RgdXuHoAd1mDetT"
60 | , scope =
61 | [ "openid", "profile" ]
62 | }
63 |
64 |
65 |
66 | --
67 | -- Model
68 | --
69 |
70 |
71 | type alias Model =
72 | { redirectUri : Url
73 | , flow : Flow
74 | }
75 |
76 |
77 | {-| This demo evolves around the following state-machine\*
78 |
79 | +--------+
80 | | Idle |
81 | +--------+
82 | |
83 | | Redirect user for authorization
84 | |
85 | v
86 | +--------------+
87 | | Authorized |
88 | +--------------+
89 | |
90 | | Exchange authorization code for an access token
91 | |
92 | v
93 | +-----------------+
94 | | Authenticated |
95 | +-----------------+
96 | |
97 | | Fetch user info using the access token
98 | v
99 | +--------+
100 | | Done |
101 | +--------+
102 |
103 | (\*) The 'Errored' state hasn't been represented here for simplicity.
104 |
105 | -}
106 | type Flow
107 | = Idle
108 | | Authorized OAuth.AuthorizationCode
109 | | Authenticated OAuth.Token
110 | | Done UserInfo
111 | | Errored Error
112 |
113 |
114 | type Error
115 | = ErrStateMismatch
116 | | ErrAuthorization OAuth.AuthorizationError
117 | | ErrAuthentication OAuth.AuthenticationError
118 | | ErrHTTPGetAccessToken
119 | | ErrHTTPGetUserInfo
120 |
121 |
122 | type alias UserInfo =
123 | { name : String
124 | , picture : String
125 | }
126 |
127 |
128 | type alias Configuration =
129 | { authorizationEndpoint : Url
130 | , tokenEndpoint : Url
131 | , userInfoEndpoint : Url
132 | , userInfoDecoder : Json.Decoder UserInfo
133 | , clientId : String
134 | , scope : List String
135 | }
136 |
137 |
138 | {-| During the authentication flow, we'll run twice into the `init` function:
139 |
140 | - The first time, for the application very first run. And we proceed with the `Idle` state,
141 | waiting for the user (a.k.a you) to request a sign in.
142 |
143 | - The second time, after a sign in has been requested, the user is redirected to the
144 | authorization server and redirects the user back to our application, with a code
145 | and other fields as query parameters.
146 |
147 | When query params are present (and valid), we consider the user `Authorized`.
148 |
149 | -}
150 | init : Maybe { state : String } -> Url -> Key -> ( Model, Cmd Msg )
151 | init mflags origin navigationKey =
152 | let
153 | redirectUri =
154 | { origin | query = Nothing, fragment = Nothing }
155 |
156 | clearUrl =
157 | Navigation.replaceUrl navigationKey (Url.toString redirectUri)
158 | in
159 | case OAuth.parseCode origin of
160 | OAuth.Empty ->
161 | ( { flow = Idle, redirectUri = redirectUri }
162 | , Cmd.none
163 | )
164 |
165 | -- It is important to set a `state` when making the authorization request
166 | -- and to verify it after the redirection. The state can be anything but its primary
167 | -- usage is to prevent cross-site request forgery; at minima, it should be a short,
168 | -- non-guessable string, generated on the fly.
169 | --
170 | -- We remember any previously generated state state using the browser's local storage
171 | -- and give it back (if present) to the elm application upon start
172 | OAuth.Success { code, state } ->
173 | case mflags of
174 | Nothing ->
175 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
176 | , clearUrl
177 | )
178 |
179 | Just flags ->
180 | if state /= Just flags.state then
181 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
182 | , clearUrl
183 | )
184 |
185 | else
186 | ( { flow = Authorized code, redirectUri = redirectUri }
187 | , Cmd.batch
188 | -- Artificial delay to make the live demo easier to follow.
189 | -- In practice, the access token could be requested right here.
190 | [ after 750 Millisecond AccessTokenRequested
191 | , clearUrl
192 | ]
193 | )
194 |
195 | OAuth.Error error ->
196 | ( { flow = Errored <| ErrAuthorization error, redirectUri = redirectUri }
197 | , clearUrl
198 | )
199 |
200 |
201 |
202 | --
203 | -- Msg
204 | --
205 |
206 |
207 | type Msg
208 | = NoOp
209 | | SignInRequested
210 | | GotRandomBytes (List Int)
211 | | AccessTokenRequested
212 | | GotAccessToken (Result Http.Error OAuth.AuthenticationSuccess)
213 | | UserInfoRequested
214 | | GotUserInfo (Result Http.Error UserInfo)
215 | | SignOutRequested
216 |
217 |
218 | getAccessToken : Configuration -> Url -> OAuth.AuthorizationCode -> Cmd Msg
219 | getAccessToken { clientId, tokenEndpoint } redirectUri code =
220 | Http.request <|
221 | OAuth.makeTokenRequest GotAccessToken
222 | { credentials =
223 | { clientId = clientId
224 | , secret = Nothing
225 | }
226 | , code = code
227 | , url = tokenEndpoint
228 | , redirectUri = redirectUri
229 | }
230 |
231 |
232 | getUserInfo : Configuration -> OAuth.Token -> Cmd Msg
233 | getUserInfo { userInfoDecoder, userInfoEndpoint } token =
234 | Http.request
235 | { method = "GET"
236 | , body = Http.emptyBody
237 | , headers = OAuth.useToken token []
238 | , url = Url.toString userInfoEndpoint
239 | , expect = Http.expectJson GotUserInfo userInfoDecoder
240 | , timeout = Nothing
241 | , tracker = Nothing
242 | }
243 |
244 |
245 |
246 | {- On the JavaScript's side, we have:
247 |
248 | app.ports.genRandomBytes.subscribe(n => {
249 | const buffer = new Uint8Array(n);
250 | crypto.getRandomValues(buffer);
251 | const bytes = Array.from(buffer);
252 | localStorage.setItem("bytes", bytes);
253 | app.ports.randomBytes.send(bytes);
254 | });
255 | -}
256 |
257 |
258 | port genRandomBytes : Int -> Cmd msg
259 |
260 |
261 | port randomBytes : (List Int -> msg) -> Sub msg
262 |
263 |
264 |
265 | --
266 | -- Update
267 | --
268 |
269 |
270 | update : Msg -> Model -> ( Model, Cmd Msg )
271 | update msg model =
272 | case ( model.flow, msg ) of
273 | ( Idle, SignInRequested ) ->
274 | signInRequested model
275 |
276 | ( Idle, GotRandomBytes bytes ) ->
277 | gotRandomBytes model bytes
278 |
279 | ( Authorized code, AccessTokenRequested ) ->
280 | accessTokenRequested model code
281 |
282 | ( Authorized _, GotAccessToken authenticationResponse ) ->
283 | gotAccessToken model authenticationResponse
284 |
285 | ( Authenticated token, UserInfoRequested ) ->
286 | userInfoRequested model token
287 |
288 | ( Authenticated _, GotUserInfo userInfoResponse ) ->
289 | gotUserInfo model userInfoResponse
290 |
291 | ( Done _, SignOutRequested ) ->
292 | signOutRequested model
293 |
294 | _ ->
295 | noOp model
296 |
297 |
298 | noOp : Model -> ( Model, Cmd Msg )
299 | noOp model =
300 | ( model, Cmd.none )
301 |
302 |
303 | signInRequested : Model -> ( Model, Cmd Msg )
304 | signInRequested model =
305 | ( { model | flow = Idle }
306 | , genRandomBytes 16
307 | )
308 |
309 |
310 | gotRandomBytes : Model -> List Int -> ( Model, Cmd Msg )
311 | gotRandomBytes model bytes =
312 | let
313 | { state } =
314 | convertBytes bytes
315 |
316 | authorization =
317 | { clientId = configuration.clientId
318 | , redirectUri = model.redirectUri
319 | , scope = configuration.scope
320 | , state = Just state
321 | , url = configuration.authorizationEndpoint
322 | }
323 | in
324 | ( { model | flow = Idle }
325 | , authorization
326 | |> OAuth.makeAuthorizationUrl
327 | |> Url.toString
328 | |> Navigation.load
329 | )
330 |
331 |
332 | accessTokenRequested : Model -> OAuth.AuthorizationCode -> ( Model, Cmd Msg )
333 | accessTokenRequested model code =
334 | ( { model | flow = Authorized code }
335 | , getAccessToken configuration model.redirectUri code
336 | )
337 |
338 |
339 | gotAccessToken : Model -> Result Http.Error OAuth.AuthenticationSuccess -> ( Model, Cmd Msg )
340 | gotAccessToken model authenticationResponse =
341 | case authenticationResponse of
342 | Err (Http.BadBody body) ->
343 | case Json.decodeString OAuth.defaultAuthenticationErrorDecoder body of
344 | Ok error ->
345 | ( { model | flow = Errored <| ErrAuthentication error }
346 | , Cmd.none
347 | )
348 |
349 | _ ->
350 | ( { model | flow = Errored ErrHTTPGetAccessToken }
351 | , Cmd.none
352 | )
353 |
354 | Err _ ->
355 | ( { model | flow = Errored ErrHTTPGetAccessToken }
356 | , Cmd.none
357 | )
358 |
359 | Ok { token } ->
360 | ( { model | flow = Authenticated token }
361 | , after 750 Millisecond UserInfoRequested
362 | )
363 |
364 |
365 | userInfoRequested : Model -> OAuth.Token -> ( Model, Cmd Msg )
366 | userInfoRequested model token =
367 | ( { model | flow = Authenticated token }
368 | , getUserInfo configuration token
369 | )
370 |
371 |
372 | gotUserInfo : Model -> Result Http.Error UserInfo -> ( Model, Cmd Msg )
373 | gotUserInfo model userInfoResponse =
374 | case userInfoResponse of
375 | Err _ ->
376 | ( { model | flow = Errored ErrHTTPGetUserInfo }
377 | , Cmd.none
378 | )
379 |
380 | Ok userInfo ->
381 | ( { model | flow = Done userInfo }
382 | , Cmd.none
383 | )
384 |
385 |
386 | signOutRequested : Model -> ( Model, Cmd Msg )
387 | signOutRequested model =
388 | ( { model | flow = Idle }
389 | , Navigation.load (Url.toString model.redirectUri)
390 | )
391 |
392 |
393 |
394 | --
395 | -- View
396 | --
397 |
398 |
399 | type alias ViewConfiguration msg =
400 | { title : String
401 | , btnClass : Attribute msg
402 | }
403 |
404 |
405 | view : ViewConfiguration Msg -> Model -> Document Msg
406 | view ({ title } as config) model =
407 | { title = title
408 | , body = viewBody config model
409 | }
410 |
411 |
412 | viewBody : ViewConfiguration Msg -> Model -> List (Html Msg)
413 | viewBody config model =
414 | [ div [ class "flex", class "flex-column", class "flex-space-around" ] <|
415 | case model.flow of
416 | Idle ->
417 | div [ class "flex" ]
418 | [ viewAuthorizationStep False
419 | , viewStepSeparator False
420 | , viewAuthenticationStep False
421 | , viewStepSeparator False
422 | , viewGetUserInfoStep False
423 | ]
424 | :: viewIdle config
425 |
426 | Authorized _ ->
427 | div [ class "flex" ]
428 | [ viewAuthorizationStep True
429 | , viewStepSeparator True
430 | , viewAuthenticationStep False
431 | , viewStepSeparator False
432 | , viewGetUserInfoStep False
433 | ]
434 | :: viewAuthorized
435 |
436 | Authenticated _ ->
437 | div [ class "flex" ]
438 | [ viewAuthorizationStep True
439 | , viewStepSeparator True
440 | , viewAuthenticationStep True
441 | , viewStepSeparator True
442 | , viewGetUserInfoStep False
443 | ]
444 | :: viewAuthenticated
445 |
446 | Done userInfo ->
447 | div [ class "flex" ]
448 | [ viewAuthorizationStep True
449 | , viewStepSeparator True
450 | , viewAuthenticationStep True
451 | , viewStepSeparator True
452 | , viewGetUserInfoStep True
453 | ]
454 | :: viewUserInfo config userInfo
455 |
456 | Errored err ->
457 | div [ class "flex" ]
458 | [ viewErroredStep
459 | ]
460 | :: viewErrored err
461 | ]
462 |
463 |
464 | viewIdle : ViewConfiguration Msg -> List (Html Msg)
465 | viewIdle { btnClass } =
466 | [ button
467 | [ onClick SignInRequested, btnClass ]
468 | [ text "Sign in" ]
469 | ]
470 |
471 |
472 | viewAuthorized : List (Html Msg)
473 | viewAuthorized =
474 | [ span [] [ text "Authenticating..." ]
475 | ]
476 |
477 |
478 | viewAuthenticated : List (Html Msg)
479 | viewAuthenticated =
480 | [ span [] [ text "Getting user info..." ]
481 | ]
482 |
483 |
484 | viewUserInfo : ViewConfiguration Msg -> UserInfo -> List (Html Msg)
485 | viewUserInfo { btnClass } { name, picture } =
486 | [ div [ class "flex", class "flex-column" ]
487 | [ img [ class "avatar", src picture ] []
488 | , p [] [ text name ]
489 | , div []
490 | [ button
491 | [ onClick SignOutRequested, btnClass ]
492 | [ text "Sign out" ]
493 | ]
494 | ]
495 | ]
496 |
497 |
498 | viewErrored : Error -> List (Html Msg)
499 | viewErrored error =
500 | [ span [ class "span-error" ] [ viewError error ] ]
501 |
502 |
503 | viewError : Error -> Html Msg
504 | viewError e =
505 | text <|
506 | case e of
507 | ErrStateMismatch ->
508 | "'state' doesn't match, the request has likely been forged by an adversary!"
509 |
510 | ErrAuthorization error ->
511 | oauthErrorToString { error = error.error, errorDescription = error.errorDescription }
512 |
513 | ErrAuthentication error ->
514 | oauthErrorToString { error = error.error, errorDescription = error.errorDescription }
515 |
516 | ErrHTTPGetAccessToken ->
517 | "Unable to retrieve token: HTTP request failed. CORS is likely disabled on the authorization server."
518 |
519 | ErrHTTPGetUserInfo ->
520 | "Unable to retrieve user info: HTTP request failed."
521 |
522 |
523 | viewAuthorizationStep : Bool -> Html Msg
524 | viewAuthorizationStep isActive =
525 | viewStep isActive ( "Authorization", style "left" "-110%" )
526 |
527 |
528 | viewAuthenticationStep : Bool -> Html Msg
529 | viewAuthenticationStep isActive =
530 | viewStep isActive ( "Authentication", style "left" "-125%" )
531 |
532 |
533 | viewGetUserInfoStep : Bool -> Html Msg
534 | viewGetUserInfoStep isActive =
535 | viewStep isActive ( "Get User Info", style "left" "-135%" )
536 |
537 |
538 | viewErroredStep : Html Msg
539 | viewErroredStep =
540 | div
541 | [ class "step", class "step-errored" ]
542 | [ span [ style "left" "-50%" ] [ text "Errored" ] ]
543 |
544 |
545 | viewStep : Bool -> ( String, Attribute Msg ) -> Html Msg
546 | viewStep isActive ( step, position ) =
547 | let
548 | stepClass =
549 | class "step"
550 | :: (if isActive then
551 | [ class "step-active" ]
552 |
553 | else
554 | []
555 | )
556 | in
557 | div stepClass [ span [ position ] [ text step ] ]
558 |
559 |
560 | viewStepSeparator : Bool -> Html Msg
561 | viewStepSeparator isActive =
562 | let
563 | stepClass =
564 | class "step-separator"
565 | :: (if isActive then
566 | [ class "step-active" ]
567 |
568 | else
569 | []
570 | )
571 | in
572 | span stepClass []
573 |
574 |
575 |
576 | --
577 | -- Helpers
578 | --
579 |
580 |
581 | toBytes : List Int -> Bytes
582 | toBytes =
583 | List.map Bytes.unsignedInt8 >> Bytes.sequence >> Bytes.encode
584 |
585 |
586 | base64 : Bytes -> String
587 | base64 =
588 | Base64.bytes >> Base64.encode
589 |
590 |
591 | convertBytes : List Int -> { state : String }
592 | convertBytes =
593 | toBytes >> base64 >> (\state -> { state = state })
594 |
595 |
596 | oauthErrorToString : { error : OAuth.ErrorCode, errorDescription : Maybe String } -> String
597 | oauthErrorToString { error, errorDescription } =
598 | let
599 | desc =
600 | errorDescription |> Maybe.withDefault "" |> String.replace "+" " "
601 | in
602 | OAuth.errorCodeToString error ++ ": " ++ desc
603 |
604 |
605 | defaultHttpsUrl : Url
606 | defaultHttpsUrl =
607 | { protocol = Https
608 | , host = ""
609 | , path = ""
610 | , port_ = Nothing
611 | , query = Nothing
612 | , fragment = Nothing
613 | }
614 |
--------------------------------------------------------------------------------
/examples/providers/auth0/authorization-code/README.md:
--------------------------------------------------------------------------------
1 | # Auth0
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/providers/auth0/authorization-code/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "."
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "andrewMacmurray/elm-delay": "3.0.0",
11 | "elm/browser": "1.0.2",
12 | "elm/bytes": "1.0.8",
13 | "elm/core": "1.0.4",
14 | "elm/html": "1.0.0",
15 | "elm/http": "2.0.0",
16 | "elm/json": "1.1.3",
17 | "elm/url": "1.0.0",
18 | "folkertdev/elm-sha2": "1.0.0",
19 | "chelovek0v/bbase64": "1.0.1"
20 | },
21 | "indirect": {
22 | "danfishgold/base64-bytes": "1.0.3",
23 | "elm/file": "1.0.5",
24 | "elm/regex": "1.0.0",
25 | "elm/time": "1.0.0",
26 | "elm/virtual-dom": "1.0.2",
27 | "rtfeldman/elm-hex": "1.0.0"
28 | }
29 | },
30 | "test-dependencies": {
31 | "direct": {},
32 | "indirect": {}
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/providers/auth0/authorization-code/src:
--------------------------------------------------------------------------------
1 | ../../../../src/
--------------------------------------------------------------------------------
/examples/providers/auth0/implicit/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing (main)
2 |
3 | import Base64.Encode as Base64
4 | import Browser exposing (Document, application)
5 | import Browser.Navigation as Navigation exposing (Key)
6 | import Bytes exposing (Bytes)
7 | import Bytes.Encode as Bytes
8 | import Delay exposing (TimeUnit(..), after)
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 | import Html.Events exposing (..)
12 | import Http
13 | import Json.Decode as Json
14 | import OAuth
15 | import OAuth.Implicit as OAuth
16 | import Url exposing (Protocol(..), Url)
17 |
18 |
19 | main : Program (Maybe (List Int)) Model Msg
20 | main =
21 | application
22 | { init =
23 | Maybe.map convertBytes >> init
24 | , update =
25 | update
26 | , subscriptions =
27 | always <| randomBytes GotRandomBytes
28 | , onUrlRequest =
29 | always NoOp
30 | , onUrlChange =
31 | always NoOp
32 | , view =
33 | view
34 | { title = "Auth0 - Flow: Implicit"
35 | , btnClass = class "btn-auth0"
36 | }
37 | }
38 |
39 |
40 | {-| OAuth configuration.
41 |
42 | Note that this demo also fetches basic user information with the obtained access token,
43 | hence the user info endpoint and JSON decoder
44 |
45 | -}
46 | configuration : Configuration
47 | configuration =
48 | { authorizationEndpoint =
49 | { defaultHttpsUrl | host = "elm-oauth2.eu.auth0.com", path = "/authorize" }
50 | , userInfoEndpoint =
51 | { defaultHttpsUrl | host = "elm-oauth2.eu.auth0.com", path = "/userinfo" }
52 | , userInfoDecoder =
53 | Json.map2 UserInfo
54 | (Json.field "name" Json.string)
55 | (Json.field "picture" Json.string)
56 | , clientId =
57 | "AbRrXEIRBPgkDrqR4RgdXuHoAd1mDetT"
58 | , scope =
59 | [ "openid", "profile" ]
60 | }
61 |
62 |
63 |
64 | --
65 | -- Model
66 | --
67 |
68 |
69 | type alias Model =
70 | { redirectUri : Url
71 | , flow : Flow
72 | }
73 |
74 |
75 | {-| This demo evolves around the following state-machine\*
76 |
77 | +--------+
78 | | Idle |
79 | +--------+
80 | |
81 | | Redirect user for authorization
82 | |
83 | v
84 | +--------------+
85 | | Authorized | w/ Access Token
86 | +--------------+
87 | |
88 | | Fetch user info using the access token
89 | v
90 | +--------+
91 | | Done |
92 | +--------+
93 |
94 | (\*) The 'Errored' state hasn't been represented here for simplicity.
95 |
96 | -}
97 | type Flow
98 | = Idle
99 | | Authorized OAuth.Token
100 | | Done UserInfo
101 | | Errored Error
102 |
103 |
104 | type Error
105 | = ErrStateMismatch
106 | | ErrAuthorization OAuth.AuthorizationError
107 | | ErrHTTPGetUserInfo
108 |
109 |
110 | type alias UserInfo =
111 | { name : String
112 | , picture : String
113 | }
114 |
115 |
116 | type alias Configuration =
117 | { authorizationEndpoint : Url
118 | , userInfoEndpoint : Url
119 | , userInfoDecoder : Json.Decoder UserInfo
120 | , clientId : String
121 | , scope : List String
122 | }
123 |
124 |
125 | {-| During the authentication flow, we'll run twice into the `init` function:
126 |
127 | - The first time, for the application very first run. And we proceed with the `Idle` state,
128 | waiting for the user (a.k.a you) to request a sign in.
129 |
130 | - The second time, after a sign in has been requested, the user is redirected to the
131 | authorization server and redirects the user back to our application, with an access
132 | token and other fields as query parameters.
133 |
134 | When query params are present (and valid), we consider the user `Authorized`.
135 |
136 | -}
137 | init : Maybe { state : String } -> Url -> Key -> ( Model, Cmd Msg )
138 | init mflags origin navigationKey =
139 | let
140 | redirectUri =
141 | { origin | query = Nothing, fragment = Nothing }
142 |
143 | clearUrl =
144 | Navigation.replaceUrl navigationKey (Url.toString redirectUri)
145 | in
146 | case OAuth.parseToken origin of
147 | OAuth.Empty ->
148 | ( { flow = Idle, redirectUri = redirectUri }
149 | , Cmd.none
150 | )
151 |
152 | -- It is important to set a `state` when making the authorization request
153 | -- and to verify it after the redirection. The state can be anything but its primary
154 | -- usage is to prevent cross-site request forgery; at minima, it should be a short,
155 | -- non-guessable string, generated on the fly.
156 | --
157 | -- We remember any previously generated state state using the browser's local storage
158 | -- and give it back (if present) to the elm application upon start
159 | OAuth.Success { token, state } ->
160 | case mflags of
161 | Nothing ->
162 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
163 | , clearUrl
164 | )
165 |
166 | Just flags ->
167 | if state /= Just flags.state then
168 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
169 | , clearUrl
170 | )
171 |
172 | else
173 | ( { flow = Authorized token, redirectUri = redirectUri }
174 | , Cmd.batch
175 | -- Artificial delay to make the live demo easier to follow.
176 | -- In practice, the access token could be requested right here.
177 | [ after 750 Millisecond UserInfoRequested
178 | , clearUrl
179 | ]
180 | )
181 |
182 | OAuth.Error error ->
183 | ( { flow = Errored <| ErrAuthorization error, redirectUri = redirectUri }
184 | , clearUrl
185 | )
186 |
187 |
188 |
189 | --
190 | -- Msg
191 | --
192 |
193 |
194 | type Msg
195 | = NoOp
196 | | SignInRequested
197 | | GotRandomBytes (List Int)
198 | | GotAccessToken (Result Http.Error OAuth.AuthorizationSuccess)
199 | | UserInfoRequested
200 | | GotUserInfo (Result Http.Error UserInfo)
201 | | SignOutRequested
202 |
203 |
204 | getUserInfo : Configuration -> OAuth.Token -> Cmd Msg
205 | getUserInfo { userInfoDecoder, userInfoEndpoint } token =
206 | Http.request
207 | { method = "GET"
208 | , body = Http.emptyBody
209 | , headers = OAuth.useToken token []
210 | , url = Url.toString userInfoEndpoint
211 | , expect = Http.expectJson GotUserInfo userInfoDecoder
212 | , timeout = Nothing
213 | , tracker = Nothing
214 | }
215 |
216 |
217 |
218 | {- On the JavaScript's side, we have:
219 |
220 | app.ports.genRandomBytes.subscribe(n => {
221 | const buffer = new Uint8Array(n);
222 | crypto.getRandomValues(buffer);
223 | const bytes = Array.from(buffer);
224 | localStorage.setItem("bytes", bytes);
225 | app.ports.randomBytes.send(bytes);
226 | });
227 | -}
228 |
229 |
230 | port genRandomBytes : Int -> Cmd msg
231 |
232 |
233 | port randomBytes : (List Int -> msg) -> Sub msg
234 |
235 |
236 |
237 | --
238 | -- Update
239 | --
240 |
241 |
242 | update : Msg -> Model -> ( Model, Cmd Msg )
243 | update msg model =
244 | case ( model.flow, msg ) of
245 | ( Idle, SignInRequested ) ->
246 | signInRequested model
247 |
248 | ( Idle, GotRandomBytes bytes ) ->
249 | gotRandomBytes model bytes
250 |
251 | ( Authorized token, UserInfoRequested ) ->
252 | userInfoRequested model token
253 |
254 | ( Authorized _, GotUserInfo userInfoResponse ) ->
255 | gotUserInfo model userInfoResponse
256 |
257 | ( Done _, SignOutRequested ) ->
258 | signOutRequested model
259 |
260 | _ ->
261 | noOp model
262 |
263 |
264 | noOp : Model -> ( Model, Cmd Msg )
265 | noOp model =
266 | ( model, Cmd.none )
267 |
268 |
269 | signInRequested : Model -> ( Model, Cmd Msg )
270 | signInRequested model =
271 | ( { model | flow = Idle }
272 | , genRandomBytes 16
273 | )
274 |
275 |
276 | gotRandomBytes : Model -> List Int -> ( Model, Cmd Msg )
277 | gotRandomBytes model bytes =
278 | let
279 | { state } =
280 | convertBytes bytes
281 |
282 | authorization =
283 | { clientId = configuration.clientId
284 | , redirectUri = model.redirectUri
285 | , scope = configuration.scope
286 | , state = Just state
287 | , url = configuration.authorizationEndpoint
288 | }
289 | in
290 | ( { model | flow = Idle }
291 | , authorization
292 | |> OAuth.makeAuthorizationUrl
293 | |> Url.toString
294 | |> Navigation.load
295 | )
296 |
297 |
298 | userInfoRequested : Model -> OAuth.Token -> ( Model, Cmd Msg )
299 | userInfoRequested model token =
300 | ( { model | flow = Authorized token }
301 | , getUserInfo configuration token
302 | )
303 |
304 |
305 | gotUserInfo : Model -> Result Http.Error UserInfo -> ( Model, Cmd Msg )
306 | gotUserInfo model userInfoResponse =
307 | case userInfoResponse of
308 | Err _ ->
309 | ( { model | flow = Errored ErrHTTPGetUserInfo }
310 | , Cmd.none
311 | )
312 |
313 | Ok userInfo ->
314 | ( { model | flow = Done userInfo }
315 | , Cmd.none
316 | )
317 |
318 |
319 | signOutRequested : Model -> ( Model, Cmd Msg )
320 | signOutRequested model =
321 | ( { model | flow = Idle }
322 | , Navigation.load (Url.toString model.redirectUri)
323 | )
324 |
325 |
326 |
327 | --
328 | -- View
329 | --
330 |
331 |
332 | type alias ViewConfiguration msg =
333 | { title : String
334 | , btnClass : Attribute msg
335 | }
336 |
337 |
338 | view : ViewConfiguration Msg -> Model -> Document Msg
339 | view ({ title } as config) model =
340 | { title = title
341 | , body = viewBody config model
342 | }
343 |
344 |
345 | viewBody : ViewConfiguration Msg -> Model -> List (Html Msg)
346 | viewBody config model =
347 | [ div [ class "flex", class "flex-column", class "flex-space-around" ] <|
348 | case model.flow of
349 | Idle ->
350 | div [ class "flex" ]
351 | [ viewAuthorizationStep False
352 | , viewStepSeparator False
353 | , viewGetUserInfoStep False
354 | ]
355 | :: viewIdle config
356 |
357 | Authorized _ ->
358 | div [ class "flex" ]
359 | [ viewAuthorizationStep True
360 | , viewStepSeparator True
361 | , viewGetUserInfoStep False
362 | ]
363 | :: viewAuthorized
364 |
365 | Done userInfo ->
366 | div [ class "flex" ]
367 | [ viewAuthorizationStep True
368 | , viewStepSeparator True
369 | , viewGetUserInfoStep True
370 | ]
371 | :: viewUserInfo config userInfo
372 |
373 | Errored err ->
374 | div [ class "flex" ]
375 | [ viewErroredStep
376 | ]
377 | :: viewErrored err
378 | ]
379 |
380 |
381 | viewIdle : ViewConfiguration Msg -> List (Html Msg)
382 | viewIdle { btnClass } =
383 | [ button
384 | [ onClick SignInRequested, btnClass ]
385 | [ text "Sign in" ]
386 | ]
387 |
388 |
389 | viewAuthorized : List (Html Msg)
390 | viewAuthorized =
391 | [ span [] [ text "Getting user info..." ]
392 | ]
393 |
394 |
395 | viewUserInfo : ViewConfiguration Msg -> UserInfo -> List (Html Msg)
396 | viewUserInfo { btnClass } { name, picture } =
397 | [ div [ class "flex", class "flex-column" ]
398 | [ img [ class "avatar", src picture ] []
399 | , p [] [ text name ]
400 | , div []
401 | [ button
402 | [ onClick SignOutRequested, btnClass ]
403 | [ text "Sign out" ]
404 | ]
405 | ]
406 | ]
407 |
408 |
409 | viewErrored : Error -> List (Html Msg)
410 | viewErrored error =
411 | [ span [ class "span-error" ] [ viewError error ] ]
412 |
413 |
414 | viewError : Error -> Html Msg
415 | viewError e =
416 | text <|
417 | case e of
418 | ErrStateMismatch ->
419 | "'state' doesn't match, the request has likely been forged by an adversary!"
420 |
421 | ErrAuthorization error ->
422 | oauthErrorToString { error = error.error, errorDescription = error.errorDescription }
423 |
424 | ErrHTTPGetUserInfo ->
425 | "Unable to retrieve user info: HTTP request failed."
426 |
427 |
428 | viewAuthorizationStep : Bool -> Html Msg
429 | viewAuthorizationStep isActive =
430 | viewStep isActive ( "Authorization", style "left" "-110%" )
431 |
432 |
433 | viewGetUserInfoStep : Bool -> Html Msg
434 | viewGetUserInfoStep isActive =
435 | viewStep isActive ( "Get User Info", style "left" "-135%" )
436 |
437 |
438 | viewErroredStep : Html Msg
439 | viewErroredStep =
440 | div
441 | [ class "step", class "step-errored" ]
442 | [ span [ style "left" "-50%" ] [ text "Errored" ] ]
443 |
444 |
445 | viewStep : Bool -> ( String, Attribute Msg ) -> Html Msg
446 | viewStep isActive ( step, position ) =
447 | let
448 | stepClass =
449 | class "step"
450 | :: (if isActive then
451 | [ class "step-active" ]
452 |
453 | else
454 | []
455 | )
456 | in
457 | div stepClass [ span [ position ] [ text step ] ]
458 |
459 |
460 | viewStepSeparator : Bool -> Html Msg
461 | viewStepSeparator isActive =
462 | let
463 | stepClass =
464 | class "step-separator"
465 | :: (if isActive then
466 | [ class "step-active" ]
467 |
468 | else
469 | []
470 | )
471 | in
472 | span stepClass []
473 |
474 |
475 |
476 | --
477 | -- Helpers
478 | --
479 |
480 |
481 | toBytes : List Int -> Bytes
482 | toBytes =
483 | List.map Bytes.unsignedInt8 >> Bytes.sequence >> Bytes.encode
484 |
485 |
486 | base64 : Bytes -> String
487 | base64 =
488 | Base64.bytes >> Base64.encode
489 |
490 |
491 | convertBytes : List Int -> { state : String }
492 | convertBytes =
493 | toBytes >> base64 >> (\state -> { state = state })
494 |
495 |
496 | oauthErrorToString : { error : OAuth.ErrorCode, errorDescription : Maybe String } -> String
497 | oauthErrorToString { error, errorDescription } =
498 | let
499 | desc =
500 | errorDescription |> Maybe.withDefault "" |> String.replace "+" " "
501 | in
502 | OAuth.errorCodeToString error ++ ": " ++ desc
503 |
504 |
505 | defaultHttpsUrl : Url
506 | defaultHttpsUrl =
507 | { protocol = Https
508 | , host = ""
509 | , path = ""
510 | , port_ = Nothing
511 | , query = Nothing
512 | , fragment = Nothing
513 | }
514 |
--------------------------------------------------------------------------------
/examples/providers/auth0/implicit/README.md:
--------------------------------------------------------------------------------
1 | # Auth0
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/providers/auth0/implicit/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "."
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "andrewMacmurray/elm-delay": "3.0.0",
11 | "elm/browser": "1.0.2",
12 | "elm/bytes": "1.0.8",
13 | "elm/core": "1.0.4",
14 | "elm/html": "1.0.0",
15 | "elm/http": "2.0.0",
16 | "elm/json": "1.1.3",
17 | "elm/url": "1.0.0",
18 | "folkertdev/elm-sha2": "1.0.0",
19 | "chelovek0v/bbase64": "1.0.1"
20 | },
21 | "indirect": {
22 | "danfishgold/base64-bytes": "1.0.3",
23 | "elm/file": "1.0.5",
24 | "elm/regex": "1.0.0",
25 | "elm/time": "1.0.0",
26 | "elm/virtual-dom": "1.0.2",
27 | "rtfeldman/elm-hex": "1.0.0"
28 | }
29 | },
30 | "test-dependencies": {
31 | "direct": {},
32 | "indirect": {}
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/providers/auth0/implicit/src:
--------------------------------------------------------------------------------
1 | ../../../../src/
--------------------------------------------------------------------------------
/examples/providers/auth0/pkce/README.md:
--------------------------------------------------------------------------------
1 | # Auth0
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/providers/auth0/pkce/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "."
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "andrewMacmurray/elm-delay": "3.0.0",
11 | "elm/browser": "1.0.2",
12 | "elm/bytes": "1.0.8",
13 | "elm/core": "1.0.4",
14 | "elm/html": "1.0.0",
15 | "elm/http": "2.0.0",
16 | "elm/json": "1.1.3",
17 | "elm/url": "1.0.0",
18 | "folkertdev/elm-sha2": "1.0.0",
19 | "chelovek0v/bbase64": "1.0.1"
20 | },
21 | "indirect": {
22 | "danfishgold/base64-bytes": "1.0.3",
23 | "elm/file": "1.0.5",
24 | "elm/regex": "1.0.0",
25 | "elm/time": "1.0.0",
26 | "elm/virtual-dom": "1.0.2",
27 | "rtfeldman/elm-hex": "1.0.0"
28 | }
29 | },
30 | "test-dependencies": {
31 | "direct": {},
32 | "indirect": {}
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/providers/auth0/pkce/src:
--------------------------------------------------------------------------------
1 | ../../../../src/
--------------------------------------------------------------------------------
/examples/providers/facebook/README.md:
--------------------------------------------------------------------------------
1 | #
Facebook
2 |
3 | ## Authorization Flows
4 |
5 | Flow | Support | Remark | Example
6 | --- | --- | --- | ---
7 | Implicit | :heavy_check_mark: | Non-standard parsers required | [live demo][implicit-demo] \| [source code][implicit-source]
8 | Authorization Code | :heavy_check_mark: | Non-standard parsers required
Requires secret key (server-side) | N/A
9 | Authorization Code w/ PKCE | :x: | \- | \-
10 | Password | :heavy_check_mark: | \- | \-
11 | Client Credentials | :heavy_check_mark: | Requires secret key (server-side) | N/A
12 |
13 | ## OAuth Configuration
14 |
15 | \- | \-
16 | --- | ---
17 | Authorization Endpoint | facebook.com/v6.0/dialog/oauth
18 | Token Endpoint | graph.facebook.com/v6.0/oauth/access_token
19 | User Info Endpoint | graph.facebook.com/v6.0/me
20 |
21 | ---
22 |
23 | :book: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow
24 |
25 |
26 | [implicit-demo]: https://truqu.github.io/elm-oauth2/facebook/implicit/
27 | [implicit-source]: https://github.com/truqu/elm-oauth2/blob/master/examples/providers/facebook/implicit/Main.elm
28 |
--------------------------------------------------------------------------------
/examples/providers/facebook/implicit/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing (main)
2 |
3 | import Base64.Encode as Base64
4 | import Browser exposing (Document, application)
5 | import Browser.Navigation as Navigation exposing (Key)
6 | import Bytes exposing (Bytes)
7 | import Bytes.Encode as Bytes
8 | import Delay exposing (TimeUnit(..), after)
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 | import Html.Events exposing (..)
12 | import Http
13 | import Json.Decode as Json
14 | import OAuth
15 | import OAuth.Implicit as OAuth
16 | import Url exposing (Protocol(..), Url)
17 | import Url.Parser.Query as Query
18 |
19 |
20 | main : Program (Maybe (List Int)) Model Msg
21 | main =
22 | application
23 | { init =
24 | Maybe.map convertBytes >> init
25 | , update =
26 | update
27 | , subscriptions =
28 | always <| randomBytes GotRandomBytes
29 | , onUrlRequest =
30 | always NoOp
31 | , onUrlChange =
32 | always NoOp
33 | , view =
34 | view
35 | { title = "Facebook - Flow: Implicit"
36 | , btnClass = class "btn-facebook"
37 | }
38 | }
39 |
40 |
41 | {-| OAuth configuration.
42 |
43 | Note that this demo also fetches basic user information with the obtained access token,
44 | hence the user info endpoint and JSON decoder
45 |
46 | -}
47 | configuration : Configuration
48 | configuration =
49 | { authorizationEndpoint =
50 | { defaultHttpsUrl | host = "facebook.com", path = "/v6.0/dialog/oauth" }
51 | , userInfoEndpoint =
52 | { defaultHttpsUrl | host = "graph.facebook.com", path = "/v6.0/me", query = Just "fields=name,picture.type(large)" }
53 | , userInfoDecoder =
54 | Json.map2 UserInfo
55 | (Json.field "name" Json.string)
56 | (Json.field "picture" <| Json.field "data" <| Json.field "url" Json.string)
57 | , clientId =
58 | "179456896198275"
59 | , scope =
60 | [ "public_profile" ]
61 | }
62 |
63 |
64 |
65 | --
66 | -- Model
67 | --
68 |
69 |
70 | type alias Model =
71 | { redirectUri : Url
72 | , flow : Flow
73 | }
74 |
75 |
76 | {-| This demo evolves around the following state-machine\*
77 |
78 | +--------+
79 | | Idle |
80 | +--------+
81 | |
82 | | Redirect user for authorization
83 | |
84 | v
85 | +--------------+
86 | | Authorized | w/ Access Token
87 | +--------------+
88 | |
89 | | Fetch user info using the access token
90 | v
91 | +--------+
92 | | Done |
93 | +--------+
94 |
95 | (\*) The 'Errored' state hasn't been represented here for simplicity.
96 |
97 | -}
98 | type Flow
99 | = Idle
100 | | Authorized OAuth.Token
101 | | Done UserInfo
102 | | Errored Error
103 |
104 |
105 | type Error
106 | = ErrStateMismatch
107 | | ErrAuthorization OAuth.AuthorizationError
108 | | ErrHTTPGetUserInfo
109 |
110 |
111 | type alias UserInfo =
112 | { name : String
113 | , picture : String
114 | }
115 |
116 |
117 | type alias Configuration =
118 | { authorizationEndpoint : Url
119 | , userInfoEndpoint : Url
120 | , userInfoDecoder : Json.Decoder UserInfo
121 | , clientId : String
122 | , scope : List String
123 | }
124 |
125 |
126 | {-| During the authentication flow, we'll run twice into the `init` function:
127 |
128 | - The first time, for the application very first run. And we proceed with the `Idle` state,
129 | waiting for the user (a.k.a you) to request a sign in.
130 |
131 | - The second time, after a sign in has been requested, the user is redirected to the
132 | authorization server and redirects the user back to our application, with an access
133 | token and other fields as query parameters.
134 |
135 | When query params are present (and valid), we consider the user `Authorized`.
136 |
137 | -}
138 | init : Maybe { state : String } -> Url -> Key -> ( Model, Cmd Msg )
139 | init mflags origin navigationKey =
140 | let
141 | redirectUri =
142 | { origin | query = Nothing, fragment = Nothing }
143 |
144 | clearUrl =
145 | Navigation.replaceUrl navigationKey (Url.toString redirectUri)
146 | in
147 | case OAuth.parseTokenWith parsers (patchUrl origin) of
148 | OAuth.Empty ->
149 | ( { flow = Idle, redirectUri = redirectUri }
150 | , Cmd.none
151 | )
152 |
153 | -- It is important to set a `state` when making the authorization request
154 | -- and to verify it after the redirection. The state can be anything but its primary
155 | -- usage is to prevent cross-site request forgery; at minima, it should be a short,
156 | -- non-guessable string, generated on the fly.
157 | --
158 | -- We remember any previously generated state state using the browser's local storage
159 | -- and give it back (if present) to the elm application upon start
160 | OAuth.Success { token, state } ->
161 | case mflags of
162 | Nothing ->
163 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
164 | , clearUrl
165 | )
166 |
167 | Just flags ->
168 | if state /= Just flags.state then
169 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
170 | , clearUrl
171 | )
172 |
173 | else
174 | ( { flow = Authorized token, redirectUri = redirectUri }
175 | , Cmd.batch
176 | -- Artificial delay to make the live demo easier to follow.
177 | -- In practice, the access token could be requested right here.
178 | [ after 750 Millisecond UserInfoRequested
179 | , clearUrl
180 | ]
181 | )
182 |
183 | OAuth.Error error ->
184 | ( { flow = Errored <| ErrAuthorization error, redirectUri = redirectUri }
185 | , clearUrl
186 | )
187 |
188 |
189 |
190 | --
191 | -- Msg
192 | --
193 |
194 |
195 | type Msg
196 | = NoOp
197 | | SignInRequested
198 | | GotRandomBytes (List Int)
199 | | GotAccessToken (Result Http.Error OAuth.AuthorizationSuccess)
200 | | UserInfoRequested
201 | | GotUserInfo (Result Http.Error UserInfo)
202 | | SignOutRequested
203 |
204 |
205 | getUserInfo : Configuration -> OAuth.Token -> Cmd Msg
206 | getUserInfo { userInfoDecoder, userInfoEndpoint } token =
207 | Http.request
208 | { method = "GET"
209 | , body = Http.emptyBody
210 | , headers = OAuth.useToken token []
211 | , url = Url.toString userInfoEndpoint
212 | , expect = Http.expectJson GotUserInfo userInfoDecoder
213 | , timeout = Nothing
214 | , tracker = Nothing
215 | }
216 |
217 |
218 |
219 | {- On the JavaScript's side, we have:
220 |
221 | app.ports.genRandomBytes.subscribe(n => {
222 | const buffer = new Uint8Array(n);
223 | crypto.getRandomValues(buffer);
224 | const bytes = Array.from(buffer);
225 | localStorage.setItem("bytes", bytes);
226 | app.ports.randomBytes.send(bytes);
227 | });
228 | -}
229 |
230 |
231 | port genRandomBytes : Int -> Cmd msg
232 |
233 |
234 | port randomBytes : (List Int -> msg) -> Sub msg
235 |
236 |
237 |
238 | --
239 | -- Update
240 | --
241 |
242 |
243 | update : Msg -> Model -> ( Model, Cmd Msg )
244 | update msg model =
245 | case ( model.flow, msg ) of
246 | ( Idle, SignInRequested ) ->
247 | signInRequested model
248 |
249 | ( Idle, GotRandomBytes bytes ) ->
250 | gotRandomBytes model bytes
251 |
252 | ( Authorized token, UserInfoRequested ) ->
253 | userInfoRequested model token
254 |
255 | ( Authorized _, GotUserInfo userInfoResponse ) ->
256 | gotUserInfo model userInfoResponse
257 |
258 | ( Done _, SignOutRequested ) ->
259 | signOutRequested model
260 |
261 | _ ->
262 | noOp model
263 |
264 |
265 | noOp : Model -> ( Model, Cmd Msg )
266 | noOp model =
267 | ( model, Cmd.none )
268 |
269 |
270 | signInRequested : Model -> ( Model, Cmd Msg )
271 | signInRequested model =
272 | ( { model | flow = Idle }
273 | , genRandomBytes 16
274 | )
275 |
276 |
277 | gotRandomBytes : Model -> List Int -> ( Model, Cmd Msg )
278 | gotRandomBytes model bytes =
279 | let
280 | { state } =
281 | convertBytes bytes
282 |
283 | authorization =
284 | { clientId = configuration.clientId
285 | , redirectUri = model.redirectUri
286 | , scope = configuration.scope
287 | , state = Just state
288 | , url = configuration.authorizationEndpoint
289 | }
290 | in
291 | ( { model | flow = Idle }
292 | , authorization
293 | |> OAuth.makeAuthorizationUrl
294 | |> Url.toString
295 | |> Navigation.load
296 | )
297 |
298 |
299 | userInfoRequested : Model -> OAuth.Token -> ( Model, Cmd Msg )
300 | userInfoRequested model token =
301 | ( { model | flow = Authorized token }
302 | , getUserInfo configuration token
303 | )
304 |
305 |
306 | gotUserInfo : Model -> Result Http.Error UserInfo -> ( Model, Cmd Msg )
307 | gotUserInfo model userInfoResponse =
308 | case userInfoResponse of
309 | Err _ ->
310 | ( { model | flow = Errored ErrHTTPGetUserInfo }
311 | , Cmd.none
312 | )
313 |
314 | Ok userInfo ->
315 | ( { model | flow = Done userInfo }
316 | , Cmd.none
317 | )
318 |
319 |
320 | signOutRequested : Model -> ( Model, Cmd Msg )
321 | signOutRequested model =
322 | ( { model | flow = Idle }
323 | , Navigation.load (Url.toString model.redirectUri)
324 | )
325 |
326 |
327 |
328 | --
329 | -- View
330 | --
331 |
332 |
333 | type alias ViewConfiguration msg =
334 | { title : String
335 | , btnClass : Attribute msg
336 | }
337 |
338 |
339 | view : ViewConfiguration Msg -> Model -> Document Msg
340 | view ({ title } as config) model =
341 | { title = title
342 | , body = viewBody config model
343 | }
344 |
345 |
346 | viewBody : ViewConfiguration Msg -> Model -> List (Html Msg)
347 | viewBody config model =
348 | [ div [ class "flex", class "flex-column", class "flex-space-around" ] <|
349 | case model.flow of
350 | Idle ->
351 | div [ class "flex" ]
352 | [ viewAuthorizationStep False
353 | , viewStepSeparator False
354 | , viewGetUserInfoStep False
355 | ]
356 | :: viewIdle config
357 |
358 | Authorized _ ->
359 | div [ class "flex" ]
360 | [ viewAuthorizationStep True
361 | , viewStepSeparator True
362 | , viewGetUserInfoStep False
363 | ]
364 | :: viewAuthorized
365 |
366 | Done userInfo ->
367 | div [ class "flex" ]
368 | [ viewAuthorizationStep True
369 | , viewStepSeparator True
370 | , viewGetUserInfoStep True
371 | ]
372 | :: viewUserInfo config userInfo
373 |
374 | Errored err ->
375 | div [ class "flex" ]
376 | [ viewErroredStep
377 | ]
378 | :: viewErrored err
379 | ]
380 |
381 |
382 | viewIdle : ViewConfiguration Msg -> List (Html Msg)
383 | viewIdle { btnClass } =
384 | [ button
385 | [ onClick SignInRequested, btnClass ]
386 | [ text "Sign in" ]
387 | ]
388 |
389 |
390 | viewAuthorized : List (Html Msg)
391 | viewAuthorized =
392 | [ span [] [ text "Getting user info..." ]
393 | ]
394 |
395 |
396 | viewUserInfo : ViewConfiguration Msg -> UserInfo -> List (Html Msg)
397 | viewUserInfo { btnClass } { name, picture } =
398 | [ div [ class "flex", class "flex-column" ]
399 | [ img [ class "avatar", src picture ] []
400 | , p [] [ text name ]
401 | , div []
402 | [ button
403 | [ onClick SignOutRequested, btnClass ]
404 | [ text "Sign out" ]
405 | ]
406 | ]
407 | ]
408 |
409 |
410 | viewErrored : Error -> List (Html Msg)
411 | viewErrored error =
412 | [ span [ class "span-error" ] [ viewError error ] ]
413 |
414 |
415 | viewError : Error -> Html Msg
416 | viewError e =
417 | text <|
418 | case e of
419 | ErrStateMismatch ->
420 | "'state' doesn't match, the request has likely been forged by an adversary!"
421 |
422 | ErrAuthorization error ->
423 | oauthErrorToString { error = error.error, errorDescription = error.errorDescription }
424 |
425 | ErrHTTPGetUserInfo ->
426 | "Unable to retrieve user info: HTTP request failed."
427 |
428 |
429 | viewAuthorizationStep : Bool -> Html Msg
430 | viewAuthorizationStep isActive =
431 | viewStep isActive ( "Authorization", style "left" "-110%" )
432 |
433 |
434 | viewGetUserInfoStep : Bool -> Html Msg
435 | viewGetUserInfoStep isActive =
436 | viewStep isActive ( "Get User Info", style "left" "-135%" )
437 |
438 |
439 | viewErroredStep : Html Msg
440 | viewErroredStep =
441 | div
442 | [ class "step", class "step-errored" ]
443 | [ span [ style "left" "-50%" ] [ text "Errored" ] ]
444 |
445 |
446 | viewStep : Bool -> ( String, Attribute Msg ) -> Html Msg
447 | viewStep isActive ( step, position ) =
448 | let
449 | stepClass =
450 | class "step"
451 | :: (if isActive then
452 | [ class "step-active" ]
453 |
454 | else
455 | []
456 | )
457 | in
458 | div stepClass [ span [ position ] [ text step ] ]
459 |
460 |
461 | viewStepSeparator : Bool -> Html Msg
462 | viewStepSeparator isActive =
463 | let
464 | stepClass =
465 | class "step-separator"
466 | :: (if isActive then
467 | [ class "step-active" ]
468 |
469 | else
470 | []
471 | )
472 | in
473 | span stepClass []
474 |
475 |
476 |
477 | --
478 | -- Facebook Wrong Implementation Work-Arounds
479 | --
480 |
481 |
482 | {-| No 'token\_type' is returned, so we have to provide a default one as Just "Bearer".
483 | -}
484 | tokenParser : Query.Parser (Maybe OAuth.Token)
485 | tokenParser =
486 | Query.map (OAuth.makeToken (Just "Bearer"))
487 | (Query.string "access_token")
488 |
489 |
490 | {-| In case of error, no 'error' field is returned, but instead we find a field named 'error\_code'
491 | -}
492 | errorParser : Query.Parser (Maybe OAuth.ErrorCode)
493 | errorParser =
494 | Query.map (Maybe.map OAuth.errorCodeFromString)
495 | (Query.string "error_code")
496 |
497 |
498 | {-| Put everything together and rely on `OAuth.parseTokenWith` instead of the default parser
499 | -}
500 | parsers : OAuth.Parsers OAuth.AuthorizationError OAuth.AuthorizationSuccess
501 | parsers =
502 | { tokenParser = tokenParser
503 | , errorParser = errorParser
504 | , authorizationSuccessParser = OAuth.defaultAuthorizationSuccessParser
505 | , authorizationErrorParser = OAuth.defaultAuthorizationErrorParser
506 | }
507 |
508 |
509 | {-| In addition, Facebook returns parameters as query parameters instead of a fragments, and sometimes, a noise fragment is present in the response. So, as a work-around, one can patch the Url to make it compliant with the original RFC specification as follows:
510 | -}
511 | patchUrl : Url -> Url
512 | patchUrl url =
513 | if url.fragment == Just "_=_" || url.fragment == Nothing then
514 | { url | fragment = url.query }
515 |
516 | else
517 | url
518 |
519 |
520 |
521 | --
522 | -- Helpers
523 | --
524 |
525 |
526 | toBytes : List Int -> Bytes
527 | toBytes =
528 | List.map Bytes.unsignedInt8 >> Bytes.sequence >> Bytes.encode
529 |
530 |
531 | base64 : Bytes -> String
532 | base64 =
533 | Base64.bytes >> Base64.encode
534 |
535 |
536 | convertBytes : List Int -> { state : String }
537 | convertBytes =
538 | toBytes >> base64 >> (\state -> { state = state })
539 |
540 |
541 | oauthErrorToString : { error : OAuth.ErrorCode, errorDescription : Maybe String } -> String
542 | oauthErrorToString { error, errorDescription } =
543 | let
544 | desc =
545 | errorDescription |> Maybe.withDefault "" |> String.replace "+" " "
546 | in
547 | OAuth.errorCodeToString error ++ ": " ++ desc
548 |
549 |
550 | defaultHttpsUrl : Url
551 | defaultHttpsUrl =
552 | { protocol = Https
553 | , host = ""
554 | , path = ""
555 | , port_ = Nothing
556 | , query = Nothing
557 | , fragment = Nothing
558 | }
559 |
--------------------------------------------------------------------------------
/examples/providers/facebook/implicit/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "."
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "andrewMacmurray/elm-delay": "3.0.0",
11 | "elm/browser": "1.0.2",
12 | "elm/bytes": "1.0.8",
13 | "elm/core": "1.0.4",
14 | "elm/html": "1.0.0",
15 | "elm/http": "2.0.0",
16 | "elm/json": "1.1.3",
17 | "elm/url": "1.0.0",
18 | "folkertdev/elm-sha2": "1.0.0",
19 | "chelovek0v/bbase64": "1.0.1"
20 | },
21 | "indirect": {
22 | "danfishgold/base64-bytes": "1.0.3",
23 | "elm/file": "1.0.5",
24 | "elm/regex": "1.0.0",
25 | "elm/time": "1.0.0",
26 | "elm/virtual-dom": "1.0.2",
27 | "rtfeldman/elm-hex": "1.0.0"
28 | }
29 | },
30 | "test-dependencies": {
31 | "direct": {},
32 | "indirect": {}
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/providers/facebook/implicit/src:
--------------------------------------------------------------------------------
1 | ../../../../src/
--------------------------------------------------------------------------------
/examples/providers/google/README.md:
--------------------------------------------------------------------------------
1 | #
Google
2 |
3 | ## Authorization Flows
4 |
5 | Flow | Support | Remark | Example
6 | --- | --- | --- | ---
7 | Implicit | :heavy_check_mark: | \- | [live demo][implicit-demo] \| [source code][implicit-source]
8 | Authorization Code | :heavy_check_mark: | Requires secret key (server-side) | N/A
9 | Authorization Code w/ PKCE | :heavy_check_mark: | Requires secret key (server-side) | N/A
10 | Password | :heavy_check_mark: | Requires secret key (server-side) | N/A
11 | Client Credentials | :heavy_check_mark: | Requires secret key (server-side) | N/A
12 |
13 | ## OAuth Configuration
14 |
15 | \- | \-
16 | --- | ---
17 | Authorization Endpoint | accounts.google.com/o/oauth2/v2/auth
18 | Token Endpoint | www.googleapis.com/oauth2/v4/token
19 | User Info Endpoint | www.googleapis.com/oauth2/v1/userinfo
20 |
21 | ---
22 |
23 | :book: https://developers.google.com/identity/protocols/OAuth2
24 |
25 |
26 | [implicit-demo]: https://truqu.github.io/elm-oauth2/google/implicit/
27 | [implicit-source]: https://github.com/truqu/elm-oauth2/blob/master/examples/providers/google/implicit/Main.elm
28 |
--------------------------------------------------------------------------------
/examples/providers/google/implicit/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing (main)
2 |
3 | import Base64.Encode as Base64
4 | import Browser exposing (Document, application)
5 | import Browser.Navigation as Navigation exposing (Key)
6 | import Bytes exposing (Bytes)
7 | import Bytes.Encode as Bytes
8 | import Delay exposing (TimeUnit(..), after)
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 | import Html.Events exposing (..)
12 | import Http
13 | import Json.Decode as Json
14 | import OAuth
15 | import OAuth.Implicit as OAuth
16 | import Url exposing (Protocol(..), Url)
17 |
18 |
19 | main : Program (Maybe (List Int)) Model Msg
20 | main =
21 | application
22 | { init =
23 | Maybe.map convertBytes >> init
24 | , update =
25 | update
26 | , subscriptions =
27 | always <| randomBytes GotRandomBytes
28 | , onUrlRequest =
29 | always NoOp
30 | , onUrlChange =
31 | always NoOp
32 | , view =
33 | view
34 | { title = "Google - Flow: Implicit"
35 | , btnClass = class "btn-google"
36 | }
37 | }
38 |
39 |
40 | {-| OAuth configuration.
41 |
42 | Note that this demo also fetches basic user information with the obtained access token,
43 | hence the user info endpoint and JSON decoder
44 |
45 | -}
46 | configuration : Configuration
47 | configuration =
48 | { authorizationEndpoint =
49 | { defaultHttpsUrl | host = "accounts.google.com", path = "/o/oauth2/v2/auth" }
50 | , userInfoEndpoint =
51 | { defaultHttpsUrl | host = "www.googleapis.com", path = "/oauth2/v1/userinfo" }
52 | , userInfoDecoder =
53 | Json.map2 UserInfo
54 | (Json.field "name" Json.string)
55 | (Json.field "picture" Json.string)
56 | , clientId =
57 | "909608474358-fkok86ks7e83c47aq01aiit47vsoh4s0.apps.googleusercontent.com"
58 | , scope =
59 | [ "profile" ]
60 | }
61 |
62 |
63 |
64 | --
65 | -- Model
66 | --
67 |
68 |
69 | type alias Model =
70 | { redirectUri : Url
71 | , flow : Flow
72 | }
73 |
74 |
75 | {-| This demo evolves around the following state-machine\*
76 |
77 | +--------+
78 | | Idle |
79 | +--------+
80 | |
81 | | Redirect user for authorization
82 | |
83 | v
84 | +--------------+
85 | | Authorized | w/ Access Token
86 | +--------------+
87 | |
88 | | Fetch user info using the access token
89 | v
90 | +--------+
91 | | Done |
92 | +--------+
93 |
94 | (\*) The 'Errored' state hasn't been represented here for simplicity.
95 |
96 | -}
97 | type Flow
98 | = Idle
99 | | Authorized OAuth.Token
100 | | Done UserInfo
101 | | Errored Error
102 |
103 |
104 | type Error
105 | = ErrStateMismatch
106 | | ErrAuthorization OAuth.AuthorizationError
107 | | ErrHTTPGetUserInfo
108 |
109 |
110 | type alias UserInfo =
111 | { name : String
112 | , picture : String
113 | }
114 |
115 |
116 | type alias Configuration =
117 | { authorizationEndpoint : Url
118 | , userInfoEndpoint : Url
119 | , userInfoDecoder : Json.Decoder UserInfo
120 | , clientId : String
121 | , scope : List String
122 | }
123 |
124 |
125 | {-| During the authentication flow, we'll run twice into the `init` function:
126 |
127 | - The first time, for the application very first run. And we proceed with the `Idle` state,
128 | waiting for the user (a.k.a you) to request a sign in.
129 |
130 | - The second time, after a sign in has been requested, the user is redirected to the
131 | authorization server and redirects the user back to our application, with an access
132 | token and other fields as query parameters.
133 |
134 | When query params are present (and valid), we consider the user `Authorized`.
135 |
136 | -}
137 | init : Maybe { state : String } -> Url -> Key -> ( Model, Cmd Msg )
138 | init mflags origin navigationKey =
139 | let
140 | redirectUri =
141 | { origin | query = Nothing, fragment = Nothing }
142 |
143 | clearUrl =
144 | Navigation.replaceUrl navigationKey (Url.toString redirectUri)
145 | in
146 | case OAuth.parseToken origin of
147 | OAuth.Empty ->
148 | ( { flow = Idle, redirectUri = redirectUri }
149 | , Cmd.none
150 | )
151 |
152 | -- It is important to set a `state` when making the authorization request
153 | -- and to verify it after the redirection. The state can be anything but its primary
154 | -- usage is to prevent cross-site request forgery; at minima, it should be a short,
155 | -- non-guessable string, generated on the fly.
156 | --
157 | -- We remember any previously generated state state using the browser's local storage
158 | -- and give it back (if present) to the elm application upon start
159 | OAuth.Success { token, state } ->
160 | case mflags of
161 | Nothing ->
162 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
163 | , clearUrl
164 | )
165 |
166 | Just flags ->
167 | if state /= Just flags.state then
168 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
169 | , clearUrl
170 | )
171 |
172 | else
173 | ( { flow = Authorized token, redirectUri = redirectUri }
174 | , Cmd.batch
175 | -- Artificial delay to make the live demo easier to follow.
176 | -- In practice, the access token could be requested right here.
177 | [ after 750 Millisecond UserInfoRequested
178 | , clearUrl
179 | ]
180 | )
181 |
182 | OAuth.Error error ->
183 | ( { flow = Errored <| ErrAuthorization error, redirectUri = redirectUri }
184 | , clearUrl
185 | )
186 |
187 |
188 |
189 | --
190 | -- Msg
191 | --
192 |
193 |
194 | type Msg
195 | = NoOp
196 | | SignInRequested
197 | | GotRandomBytes (List Int)
198 | | GotAccessToken (Result Http.Error OAuth.AuthorizationSuccess)
199 | | UserInfoRequested
200 | | GotUserInfo (Result Http.Error UserInfo)
201 | | SignOutRequested
202 |
203 |
204 | getUserInfo : Configuration -> OAuth.Token -> Cmd Msg
205 | getUserInfo { userInfoDecoder, userInfoEndpoint } token =
206 | Http.request
207 | { method = "GET"
208 | , body = Http.emptyBody
209 | , headers = OAuth.useToken token []
210 | , url = Url.toString userInfoEndpoint
211 | , expect = Http.expectJson GotUserInfo userInfoDecoder
212 | , timeout = Nothing
213 | , tracker = Nothing
214 | }
215 |
216 |
217 |
218 | {- On the JavaScript's side, we have:
219 |
220 | app.ports.genRandomBytes.subscribe(n => {
221 | const buffer = new Uint8Array(n);
222 | crypto.getRandomValues(buffer);
223 | const bytes = Array.from(buffer);
224 | localStorage.setItem("bytes", bytes);
225 | app.ports.randomBytes.send(bytes);
226 | });
227 | -}
228 |
229 |
230 | port genRandomBytes : Int -> Cmd msg
231 |
232 |
233 | port randomBytes : (List Int -> msg) -> Sub msg
234 |
235 |
236 |
237 | --
238 | -- Update
239 | --
240 |
241 |
242 | update : Msg -> Model -> ( Model, Cmd Msg )
243 | update msg model =
244 | case ( model.flow, msg ) of
245 | ( Idle, SignInRequested ) ->
246 | signInRequested model
247 |
248 | ( Idle, GotRandomBytes bytes ) ->
249 | gotRandomBytes model bytes
250 |
251 | ( Authorized token, UserInfoRequested ) ->
252 | userInfoRequested model token
253 |
254 | ( Authorized _, GotUserInfo userInfoResponse ) ->
255 | gotUserInfo model userInfoResponse
256 |
257 | ( Done _, SignOutRequested ) ->
258 | signOutRequested model
259 |
260 | _ ->
261 | noOp model
262 |
263 |
264 | noOp : Model -> ( Model, Cmd Msg )
265 | noOp model =
266 | ( model, Cmd.none )
267 |
268 |
269 | signInRequested : Model -> ( Model, Cmd Msg )
270 | signInRequested model =
271 | ( { model | flow = Idle }
272 | , genRandomBytes 16
273 | )
274 |
275 |
276 | gotRandomBytes : Model -> List Int -> ( Model, Cmd Msg )
277 | gotRandomBytes model bytes =
278 | let
279 | { state } =
280 | convertBytes bytes
281 |
282 | authorization =
283 | { clientId = configuration.clientId
284 | , redirectUri = model.redirectUri
285 | , scope = configuration.scope
286 | , state = Just state
287 | , url = configuration.authorizationEndpoint
288 | }
289 | in
290 | ( { model | flow = Idle }
291 | , authorization
292 | |> OAuth.makeAuthorizationUrl
293 | |> Url.toString
294 | |> Navigation.load
295 | )
296 |
297 |
298 | userInfoRequested : Model -> OAuth.Token -> ( Model, Cmd Msg )
299 | userInfoRequested model token =
300 | ( { model | flow = Authorized token }
301 | , getUserInfo configuration token
302 | )
303 |
304 |
305 | gotUserInfo : Model -> Result Http.Error UserInfo -> ( Model, Cmd Msg )
306 | gotUserInfo model userInfoResponse =
307 | case userInfoResponse of
308 | Err _ ->
309 | ( { model | flow = Errored ErrHTTPGetUserInfo }
310 | , Cmd.none
311 | )
312 |
313 | Ok userInfo ->
314 | ( { model | flow = Done userInfo }
315 | , Cmd.none
316 | )
317 |
318 |
319 | signOutRequested : Model -> ( Model, Cmd Msg )
320 | signOutRequested model =
321 | ( { model | flow = Idle }
322 | , Navigation.load (Url.toString model.redirectUri)
323 | )
324 |
325 |
326 |
327 | --
328 | -- View
329 | --
330 |
331 |
332 | type alias ViewConfiguration msg =
333 | { title : String
334 | , btnClass : Attribute msg
335 | }
336 |
337 |
338 | view : ViewConfiguration Msg -> Model -> Document Msg
339 | view ({ title } as config) model =
340 | { title = title
341 | , body = viewBody config model
342 | }
343 |
344 |
345 | viewBody : ViewConfiguration Msg -> Model -> List (Html Msg)
346 | viewBody config model =
347 | [ div [ class "flex", class "flex-column", class "flex-space-around" ] <|
348 | case model.flow of
349 | Idle ->
350 | div [ class "flex" ]
351 | [ viewAuthorizationStep False
352 | , viewStepSeparator False
353 | , viewGetUserInfoStep False
354 | ]
355 | :: viewIdle config
356 |
357 | Authorized _ ->
358 | div [ class "flex" ]
359 | [ viewAuthorizationStep True
360 | , viewStepSeparator True
361 | , viewGetUserInfoStep False
362 | ]
363 | :: viewAuthorized
364 |
365 | Done userInfo ->
366 | div [ class "flex" ]
367 | [ viewAuthorizationStep True
368 | , viewStepSeparator True
369 | , viewGetUserInfoStep True
370 | ]
371 | :: viewUserInfo config userInfo
372 |
373 | Errored err ->
374 | div [ class "flex" ]
375 | [ viewErroredStep
376 | ]
377 | :: viewErrored err
378 | ]
379 |
380 |
381 | viewIdle : ViewConfiguration Msg -> List (Html Msg)
382 | viewIdle { btnClass } =
383 | [ button
384 | [ onClick SignInRequested, btnClass ]
385 | [ text "Sign in" ]
386 | ]
387 |
388 |
389 | viewAuthorized : List (Html Msg)
390 | viewAuthorized =
391 | [ span [] [ text "Getting user info..." ]
392 | ]
393 |
394 |
395 | viewUserInfo : ViewConfiguration Msg -> UserInfo -> List (Html Msg)
396 | viewUserInfo { btnClass } { name, picture } =
397 | [ div [ class "flex", class "flex-column" ]
398 | [ img [ class "avatar", src picture ] []
399 | , p [] [ text name ]
400 | , div []
401 | [ button
402 | [ onClick SignOutRequested, btnClass ]
403 | [ text "Sign out" ]
404 | ]
405 | ]
406 | ]
407 |
408 |
409 | viewErrored : Error -> List (Html Msg)
410 | viewErrored error =
411 | [ span [ class "span-error" ] [ viewError error ] ]
412 |
413 |
414 | viewError : Error -> Html Msg
415 | viewError e =
416 | text <|
417 | case e of
418 | ErrStateMismatch ->
419 | "'state' doesn't match, the request has likely been forged by an adversary!"
420 |
421 | ErrAuthorization error ->
422 | oauthErrorToString { error = error.error, errorDescription = error.errorDescription }
423 |
424 | ErrHTTPGetUserInfo ->
425 | "Unable to retrieve user info: HTTP request failed."
426 |
427 |
428 | viewAuthorizationStep : Bool -> Html Msg
429 | viewAuthorizationStep isActive =
430 | viewStep isActive ( "Authorization", style "left" "-110%" )
431 |
432 |
433 | viewGetUserInfoStep : Bool -> Html Msg
434 | viewGetUserInfoStep isActive =
435 | viewStep isActive ( "Get User Info", style "left" "-135%" )
436 |
437 |
438 | viewErroredStep : Html Msg
439 | viewErroredStep =
440 | div
441 | [ class "step", class "step-errored" ]
442 | [ span [ style "left" "-50%" ] [ text "Errored" ] ]
443 |
444 |
445 | viewStep : Bool -> ( String, Attribute Msg ) -> Html Msg
446 | viewStep isActive ( step, position ) =
447 | let
448 | stepClass =
449 | class "step"
450 | :: (if isActive then
451 | [ class "step-active" ]
452 |
453 | else
454 | []
455 | )
456 | in
457 | div stepClass [ span [ position ] [ text step ] ]
458 |
459 |
460 | viewStepSeparator : Bool -> Html Msg
461 | viewStepSeparator isActive =
462 | let
463 | stepClass =
464 | class "step-separator"
465 | :: (if isActive then
466 | [ class "step-active" ]
467 |
468 | else
469 | []
470 | )
471 | in
472 | span stepClass []
473 |
474 |
475 |
476 | --
477 | -- Helpers
478 | --
479 |
480 |
481 | toBytes : List Int -> Bytes
482 | toBytes =
483 | List.map Bytes.unsignedInt8 >> Bytes.sequence >> Bytes.encode
484 |
485 |
486 | base64 : Bytes -> String
487 | base64 =
488 | Base64.bytes >> Base64.encode
489 |
490 |
491 | convertBytes : List Int -> { state : String }
492 | convertBytes =
493 | toBytes >> base64 >> (\state -> { state = state })
494 |
495 |
496 | oauthErrorToString : { error : OAuth.ErrorCode, errorDescription : Maybe String } -> String
497 | oauthErrorToString { error, errorDescription } =
498 | let
499 | desc =
500 | errorDescription |> Maybe.withDefault "" |> String.replace "+" " "
501 | in
502 | OAuth.errorCodeToString error ++ ": " ++ desc
503 |
504 |
505 | defaultHttpsUrl : Url
506 | defaultHttpsUrl =
507 | { protocol = Https
508 | , host = ""
509 | , path = ""
510 | , port_ = Nothing
511 | , query = Nothing
512 | , fragment = Nothing
513 | }
514 |
--------------------------------------------------------------------------------
/examples/providers/google/implicit/README.md:
--------------------------------------------------------------------------------
1 | # Auth0
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/providers/google/implicit/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "."
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "andrewMacmurray/elm-delay": "3.0.0",
11 | "elm/browser": "1.0.2",
12 | "elm/bytes": "1.0.8",
13 | "elm/core": "1.0.4",
14 | "elm/html": "1.0.0",
15 | "elm/http": "2.0.0",
16 | "elm/json": "1.1.3",
17 | "elm/url": "1.0.0",
18 | "folkertdev/elm-sha2": "1.0.0",
19 | "chelovek0v/bbase64": "1.0.1"
20 | },
21 | "indirect": {
22 | "danfishgold/base64-bytes": "1.0.3",
23 | "elm/file": "1.0.5",
24 | "elm/regex": "1.0.0",
25 | "elm/time": "1.0.0",
26 | "elm/virtual-dom": "1.0.2",
27 | "rtfeldman/elm-hex": "1.0.0"
28 | }
29 | },
30 | "test-dependencies": {
31 | "direct": {},
32 | "indirect": {}
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/providers/google/implicit/src:
--------------------------------------------------------------------------------
1 | ../../../../src/
--------------------------------------------------------------------------------
/examples/providers/spotify/implicit/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing (main)
2 |
3 | import Base64.Encode as Base64
4 | import Browser exposing (Document, application)
5 | import Browser.Navigation as Navigation exposing (Key)
6 | import Bytes exposing (Bytes)
7 | import Bytes.Encode as Bytes
8 | import Delay exposing (TimeUnit(..), after)
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 | import Html.Events exposing (..)
12 | import Http
13 | import Json.Decode as Json
14 | import OAuth
15 | import OAuth.Implicit as OAuth
16 | import Url exposing (Protocol(..), Url)
17 |
18 |
19 | main : Program (Maybe (List Int)) Model Msg
20 | main =
21 | application
22 | { init =
23 | Maybe.map convertBytes >> init
24 | , update =
25 | update
26 | , subscriptions =
27 | always <| randomBytes GotRandomBytes
28 | , onUrlRequest =
29 | always NoOp
30 | , onUrlChange =
31 | always NoOp
32 | , view =
33 | view
34 | { title = "Spotify - Flow: Implicit"
35 | , btnClass = class "btn-spotify"
36 | }
37 | }
38 |
39 |
40 | {-| OAuth configuration.
41 |
42 | Note that this demo also fetches basic user information with the obtained access token,
43 | hence the user info endpoint and JSON decoder
44 |
45 | -}
46 | configuration : Configuration
47 | configuration =
48 | { authorizationEndpoint =
49 | { defaultHttpsUrl | host = "accounts.spotify.com", path = "/authorize" }
50 | , userInfoEndpoint =
51 | { defaultHttpsUrl | host = "api.spotify.com", path = "/v1/me" }
52 | , userInfoDecoder =
53 | Json.map2 UserInfo
54 | (Json.field "display_name" Json.string)
55 | (Json.field "images" <| Json.index 0 <| Json.field "url" Json.string)
56 | , clientId =
57 | "391d08ef3d7a46558493cb822a991dbb"
58 | , scope =
59 | []
60 | }
61 |
62 |
63 |
64 | --
65 | -- Model
66 | --
67 |
68 |
69 | type alias Model =
70 | { redirectUri : Url
71 | , flow : Flow
72 | }
73 |
74 |
75 | {-| This demo evolves around the following state-machine\*
76 |
77 | +--------+
78 | | Idle |
79 | +--------+
80 | |
81 | | Redirect user for authorization
82 | |
83 | v
84 | +--------------+
85 | | Authorized | w/ Access Token
86 | +--------------+
87 | |
88 | | Fetch user info using the access token
89 | v
90 | +--------+
91 | | Done |
92 | +--------+
93 |
94 | (\*) The 'Errored' state hasn't been represented here for simplicity.
95 |
96 | -}
97 | type Flow
98 | = Idle
99 | | Authorized OAuth.Token
100 | | Done UserInfo
101 | | Errored Error
102 |
103 |
104 | type Error
105 | = ErrStateMismatch
106 | | ErrAuthorization OAuth.AuthorizationError
107 | | ErrHTTPGetUserInfo
108 |
109 |
110 | type alias UserInfo =
111 | { name : String
112 | , picture : String
113 | }
114 |
115 |
116 | type alias Configuration =
117 | { authorizationEndpoint : Url
118 | , userInfoEndpoint : Url
119 | , userInfoDecoder : Json.Decoder UserInfo
120 | , clientId : String
121 | , scope : List String
122 | }
123 |
124 |
125 | {-| During the authentication flow, we'll run twice into the `init` function:
126 |
127 | - The first time, for the application very first run. And we proceed with the `Idle` state,
128 | waiting for the user (a.k.a you) to request a sign in.
129 |
130 | - The second time, after a sign in has been requested, the user is redirected to the
131 | authorization server and redirects the user back to our application, with an access
132 | token and other fields as query parameters.
133 |
134 | When query params are present (and valid), we consider the user `Authorized`.
135 |
136 | -}
137 | init : Maybe { state : String } -> Url -> Key -> ( Model, Cmd Msg )
138 | init mflags origin navigationKey =
139 | let
140 | redirectUri =
141 | { origin | query = Nothing, fragment = Nothing }
142 |
143 | clearUrl =
144 | Navigation.replaceUrl navigationKey (Url.toString redirectUri)
145 | in
146 | case OAuth.parseToken origin of
147 | OAuth.Empty ->
148 | ( { flow = Idle, redirectUri = redirectUri }
149 | , Cmd.none
150 | )
151 |
152 | -- It is important to set a `state` when making the authorization request
153 | -- and to verify it after the redirection. The state can be anything but its primary
154 | -- usage is to prevent cross-site request forgery; at minima, it should be a short,
155 | -- non-guessable string, generated on the fly.
156 | --
157 | -- We remember any previously generated state state using the browser's local storage
158 | -- and give it back (if present) to the elm application upon start
159 | OAuth.Success { token, state } ->
160 | case mflags of
161 | Nothing ->
162 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
163 | , clearUrl
164 | )
165 |
166 | Just flags ->
167 | if state /= Just flags.state then
168 | ( { flow = Errored ErrStateMismatch, redirectUri = redirectUri }
169 | , clearUrl
170 | )
171 |
172 | else
173 | ( { flow = Authorized token, redirectUri = redirectUri }
174 | , Cmd.batch
175 | -- Artificial delay to make the live demo easier to follow.
176 | -- In practice, the access token could be requested right here.
177 | [ after 750 Millisecond UserInfoRequested
178 | , clearUrl
179 | ]
180 | )
181 |
182 | OAuth.Error error ->
183 | ( { flow = Errored <| ErrAuthorization error, redirectUri = redirectUri }
184 | , clearUrl
185 | )
186 |
187 |
188 |
189 | --
190 | -- Msg
191 | --
192 |
193 |
194 | type Msg
195 | = NoOp
196 | | SignInRequested
197 | | GotRandomBytes (List Int)
198 | | GotAccessToken (Result Http.Error OAuth.AuthorizationSuccess)
199 | | UserInfoRequested
200 | | GotUserInfo (Result Http.Error UserInfo)
201 | | SignOutRequested
202 |
203 |
204 | getUserInfo : Configuration -> OAuth.Token -> Cmd Msg
205 | getUserInfo { userInfoDecoder, userInfoEndpoint } token =
206 | Http.request
207 | { method = "GET"
208 | , body = Http.emptyBody
209 | , headers = OAuth.useToken token []
210 | , url = Url.toString userInfoEndpoint
211 | , expect = Http.expectJson GotUserInfo userInfoDecoder
212 | , timeout = Nothing
213 | , tracker = Nothing
214 | }
215 |
216 |
217 |
218 | {- On the JavaScript's side, we have:
219 |
220 | app.ports.genRandomBytes.subscribe(n => {
221 | const buffer = new Uint8Array(n);
222 | crypto.getRandomValues(buffer);
223 | const bytes = Array.from(buffer);
224 | localStorage.setItem("bytes", bytes);
225 | app.ports.randomBytes.send(bytes);
226 | });
227 | -}
228 |
229 |
230 | port genRandomBytes : Int -> Cmd msg
231 |
232 |
233 | port randomBytes : (List Int -> msg) -> Sub msg
234 |
235 |
236 |
237 | --
238 | -- Update
239 | --
240 |
241 |
242 | update : Msg -> Model -> ( Model, Cmd Msg )
243 | update msg model =
244 | case ( model.flow, msg ) of
245 | ( Idle, SignInRequested ) ->
246 | signInRequested model
247 |
248 | ( Idle, GotRandomBytes bytes ) ->
249 | gotRandomBytes model bytes
250 |
251 | ( Authorized token, UserInfoRequested ) ->
252 | userInfoRequested model token
253 |
254 | ( Authorized _, GotUserInfo userInfoResponse ) ->
255 | gotUserInfo model userInfoResponse
256 |
257 | ( Done _, SignOutRequested ) ->
258 | signOutRequested model
259 |
260 | _ ->
261 | noOp model
262 |
263 |
264 | noOp : Model -> ( Model, Cmd Msg )
265 | noOp model =
266 | ( model, Cmd.none )
267 |
268 |
269 | signInRequested : Model -> ( Model, Cmd Msg )
270 | signInRequested model =
271 | ( { model | flow = Idle }
272 | , genRandomBytes 16
273 | )
274 |
275 |
276 | gotRandomBytes : Model -> List Int -> ( Model, Cmd Msg )
277 | gotRandomBytes model bytes =
278 | let
279 | { state } =
280 | convertBytes bytes
281 |
282 | authorization =
283 | { clientId = configuration.clientId
284 | , redirectUri = model.redirectUri
285 | , scope = configuration.scope
286 | , state = Just state
287 | , url = configuration.authorizationEndpoint
288 | }
289 | in
290 | ( { model | flow = Idle }
291 | , authorization
292 | |> OAuth.makeAuthorizationUrl
293 | |> Url.toString
294 | |> Navigation.load
295 | )
296 |
297 |
298 | userInfoRequested : Model -> OAuth.Token -> ( Model, Cmd Msg )
299 | userInfoRequested model token =
300 | ( { model | flow = Authorized token }
301 | , getUserInfo configuration token
302 | )
303 |
304 |
305 | gotUserInfo : Model -> Result Http.Error UserInfo -> ( Model, Cmd Msg )
306 | gotUserInfo model userInfoResponse =
307 | case userInfoResponse of
308 | Err _ ->
309 | ( { model | flow = Errored ErrHTTPGetUserInfo }
310 | , Cmd.none
311 | )
312 |
313 | Ok userInfo ->
314 | ( { model | flow = Done userInfo }
315 | , Cmd.none
316 | )
317 |
318 |
319 | signOutRequested : Model -> ( Model, Cmd Msg )
320 | signOutRequested model =
321 | ( { model | flow = Idle }
322 | , Navigation.load (Url.toString model.redirectUri)
323 | )
324 |
325 |
326 |
327 | --
328 | -- View
329 | --
330 |
331 |
332 | type alias ViewConfiguration msg =
333 | { title : String
334 | , btnClass : Attribute msg
335 | }
336 |
337 |
338 | view : ViewConfiguration Msg -> Model -> Document Msg
339 | view ({ title } as config) model =
340 | { title = title
341 | , body = viewBody config model
342 | }
343 |
344 |
345 | viewBody : ViewConfiguration Msg -> Model -> List (Html Msg)
346 | viewBody config model =
347 | [ div [ class "flex", class "flex-column", class "flex-space-around" ] <|
348 | case model.flow of
349 | Idle ->
350 | div [ class "flex" ]
351 | [ viewAuthorizationStep False
352 | , viewStepSeparator False
353 | , viewGetUserInfoStep False
354 | ]
355 | :: viewIdle config
356 |
357 | Authorized _ ->
358 | div [ class "flex" ]
359 | [ viewAuthorizationStep True
360 | , viewStepSeparator True
361 | , viewGetUserInfoStep False
362 | ]
363 | :: viewAuthorized
364 |
365 | Done userInfo ->
366 | div [ class "flex" ]
367 | [ viewAuthorizationStep True
368 | , viewStepSeparator True
369 | , viewGetUserInfoStep True
370 | ]
371 | :: viewUserInfo config userInfo
372 |
373 | Errored err ->
374 | div [ class "flex" ]
375 | [ viewErroredStep
376 | ]
377 | :: viewErrored err
378 | ]
379 |
380 |
381 | viewIdle : ViewConfiguration Msg -> List (Html Msg)
382 | viewIdle { btnClass } =
383 | [ button
384 | [ onClick SignInRequested, btnClass ]
385 | [ text "Sign in" ]
386 | ]
387 |
388 |
389 | viewAuthorized : List (Html Msg)
390 | viewAuthorized =
391 | [ span [] [ text "Getting user info..." ]
392 | ]
393 |
394 |
395 | viewUserInfo : ViewConfiguration Msg -> UserInfo -> List (Html Msg)
396 | viewUserInfo { btnClass } { name, picture } =
397 | [ div [ class "flex", class "flex-column" ]
398 | [ img [ class "avatar", src picture ] []
399 | , p [] [ text name ]
400 | , div []
401 | [ button
402 | [ onClick SignOutRequested, btnClass ]
403 | [ text "Sign out" ]
404 | ]
405 | ]
406 | ]
407 |
408 |
409 | viewErrored : Error -> List (Html Msg)
410 | viewErrored error =
411 | [ span [ class "span-error" ] [ viewError error ] ]
412 |
413 |
414 | viewError : Error -> Html Msg
415 | viewError e =
416 | text <|
417 | case e of
418 | ErrStateMismatch ->
419 | "'state' doesn't match, the request has likely been forged by an adversary!"
420 |
421 | ErrAuthorization error ->
422 | oauthErrorToString { error = error.error, errorDescription = error.errorDescription }
423 |
424 | ErrHTTPGetUserInfo ->
425 | "Unable to retrieve user info: HTTP request failed."
426 |
427 |
428 | viewAuthorizationStep : Bool -> Html Msg
429 | viewAuthorizationStep isActive =
430 | viewStep isActive ( "Authorization", style "left" "-110%" )
431 |
432 |
433 | viewGetUserInfoStep : Bool -> Html Msg
434 | viewGetUserInfoStep isActive =
435 | viewStep isActive ( "Get User Info", style "left" "-135%" )
436 |
437 |
438 | viewErroredStep : Html Msg
439 | viewErroredStep =
440 | div
441 | [ class "step", class "step-errored" ]
442 | [ span [ style "left" "-50%" ] [ text "Errored" ] ]
443 |
444 |
445 | viewStep : Bool -> ( String, Attribute Msg ) -> Html Msg
446 | viewStep isActive ( step, position ) =
447 | let
448 | stepClass =
449 | class "step"
450 | :: (if isActive then
451 | [ class "step-active" ]
452 |
453 | else
454 | []
455 | )
456 | in
457 | div stepClass [ span [ position ] [ text step ] ]
458 |
459 |
460 | viewStepSeparator : Bool -> Html Msg
461 | viewStepSeparator isActive =
462 | let
463 | stepClass =
464 | class "step-separator"
465 | :: (if isActive then
466 | [ class "step-active" ]
467 |
468 | else
469 | []
470 | )
471 | in
472 | span stepClass []
473 |
474 |
475 |
476 | --
477 | -- Helpers
478 | --
479 |
480 |
481 | toBytes : List Int -> Bytes
482 | toBytes =
483 | List.map Bytes.unsignedInt8 >> Bytes.sequence >> Bytes.encode
484 |
485 |
486 | base64 : Bytes -> String
487 | base64 =
488 | Base64.bytes >> Base64.encode
489 |
490 |
491 | convertBytes : List Int -> { state : String }
492 | convertBytes =
493 | toBytes >> base64 >> (\state -> { state = state })
494 |
495 |
496 | oauthErrorToString : { error : OAuth.ErrorCode, errorDescription : Maybe String } -> String
497 | oauthErrorToString { error, errorDescription } =
498 | let
499 | desc =
500 | errorDescription |> Maybe.withDefault "" |> String.replace "+" " "
501 | in
502 | OAuth.errorCodeToString error ++ ": " ++ desc
503 |
504 |
505 | defaultHttpsUrl : Url
506 | defaultHttpsUrl =
507 | { protocol = Https
508 | , host = ""
509 | , path = ""
510 | , port_ = Nothing
511 | , query = Nothing
512 | , fragment = Nothing
513 | }
514 |
--------------------------------------------------------------------------------
/examples/providers/spotify/implicit/README.md:
--------------------------------------------------------------------------------
1 | # Auth0
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/providers/spotify/implicit/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "."
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "andrewMacmurray/elm-delay": "3.0.0",
11 | "elm/browser": "1.0.2",
12 | "elm/bytes": "1.0.8",
13 | "elm/core": "1.0.4",
14 | "elm/html": "1.0.0",
15 | "elm/http": "2.0.0",
16 | "elm/json": "1.1.3",
17 | "elm/url": "1.0.0",
18 | "folkertdev/elm-sha2": "1.0.0",
19 | "chelovek0v/bbase64": "1.0.1"
20 | },
21 | "indirect": {
22 | "danfishgold/base64-bytes": "1.0.3",
23 | "elm/file": "1.0.5",
24 | "elm/regex": "1.0.0",
25 | "elm/time": "1.0.0",
26 | "elm/virtual-dom": "1.0.2",
27 | "rtfeldman/elm-hex": "1.0.0"
28 | }
29 | },
30 | "test-dependencies": {
31 | "direct": {},
32 | "indirect": {}
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/providers/spotify/implicit/src:
--------------------------------------------------------------------------------
1 | ../../../../src/
--------------------------------------------------------------------------------
/src/Extra/Maybe.elm:
--------------------------------------------------------------------------------
1 | module Extra.Maybe exposing (andThen2)
2 |
3 | {-| Extra helpers for `Maybe`
4 |
5 | @docs andThen2
6 |
7 | -}
8 |
9 |
10 | andThen2 : (a -> b -> Maybe c) -> Maybe a -> Maybe b -> Maybe c
11 | andThen2 fn ma mb =
12 | Maybe.andThen identity (Maybe.map2 fn ma mb)
13 |
--------------------------------------------------------------------------------
/src/Internal.elm:
--------------------------------------------------------------------------------
1 | module Internal exposing
2 | ( AuthenticationError
3 | , AuthenticationSuccess
4 | , Authorization
5 | , AuthorizationError
6 | , RequestParts
7 | , authenticationErrorDecoder
8 | , authenticationSuccessDecoder
9 | , authorizationErrorParser
10 | , decoderFromJust
11 | , decoderFromResult
12 | , errorDecoder
13 | , errorDescriptionDecoder
14 | , errorDescriptionParser
15 | , errorParser
16 | , errorUriDecoder
17 | , errorUriParser
18 | , expiresInDecoder
19 | , expiresInParser
20 | , extractTokenString
21 | , lenientScopeDecoder
22 | , makeAuthorizationUrl
23 | , makeHeaders
24 | , makeRedirectUri
25 | , makeRequest
26 | , parseUrlQuery
27 | , protocolToString
28 | , refreshTokenDecoder
29 | , scopeDecoder
30 | , scopeParser
31 | , spaceSeparatedListParser
32 | , stateParser
33 | , tokenDecoder
34 | , tokenParser
35 | , urlAddExtraFields
36 | , urlAddList
37 | , urlAddMaybe
38 | )
39 |
40 | import Base64.Encode as Base64
41 | import Dict as Dict exposing (Dict)
42 | import Http as Http
43 | import Json.Decode as Json
44 | import OAuth exposing (..)
45 | import Url exposing (Protocol(..), Url)
46 | import Url.Builder as Builder exposing (QueryParameter)
47 | import Url.Parser as Url
48 | import Url.Parser.Query as Query
49 |
50 |
51 |
52 | --
53 | -- Json Decoders
54 | --
55 |
56 |
57 | {-| Json decoder for a response. You may provide a custom response decoder using other decoders
58 | from this module, or some of your own craft.
59 | -}
60 | authenticationSuccessDecoder : Json.Decoder AuthenticationSuccess
61 | authenticationSuccessDecoder =
62 | Json.map4 AuthenticationSuccess
63 | tokenDecoder
64 | refreshTokenDecoder
65 | expiresInDecoder
66 | scopeDecoder
67 |
68 |
69 | authenticationErrorDecoder : Json.Decoder e -> Json.Decoder (AuthenticationError e)
70 | authenticationErrorDecoder errorCodeDecoder =
71 | Json.map3 AuthenticationError
72 | errorCodeDecoder
73 | errorDescriptionDecoder
74 | errorUriDecoder
75 |
76 |
77 | {-| Json decoder for an expire timestamp
78 | -}
79 | expiresInDecoder : Json.Decoder (Maybe Int)
80 | expiresInDecoder =
81 | Json.maybe <| Json.field "expires_in" Json.int
82 |
83 |
84 | {-| Json decoder for a scope
85 | -}
86 | scopeDecoder : Json.Decoder (List String)
87 | scopeDecoder =
88 | Json.map (Maybe.withDefault []) <| Json.maybe <| Json.field "scope" (Json.list Json.string)
89 |
90 |
91 | {-| Json decoder for a scope, allowing comma- or space-separated scopes
92 | -}
93 | lenientScopeDecoder : Json.Decoder (List String)
94 | lenientScopeDecoder =
95 | Json.map (Maybe.withDefault []) <|
96 | Json.maybe <|
97 | Json.field "scope" <|
98 | Json.oneOf
99 | [ Json.list Json.string
100 | , Json.map (String.split ",") Json.string
101 | ]
102 |
103 |
104 | {-| Json decoder for an access token
105 | -}
106 | tokenDecoder : Json.Decoder Token
107 | tokenDecoder =
108 | Json.andThen (decoderFromJust "missing or invalid 'access_token' / 'token_type'") <|
109 | Json.map2 makeToken
110 | (Json.field "token_type" Json.string |> Json.map Just)
111 | (Json.field "access_token" Json.string |> Json.map Just)
112 |
113 |
114 | {-| Json decoder for a refresh token
115 | -}
116 | refreshTokenDecoder : Json.Decoder (Maybe Token)
117 | refreshTokenDecoder =
118 | Json.andThen (decoderFromJust "missing or invalid 'refresh_token' / 'token_type'") <|
119 | Json.map2 makeRefreshToken
120 | (Json.field "token_type" Json.string)
121 | (Json.field "refresh_token" Json.string |> Json.maybe)
122 |
123 |
124 | {-| Json decoder for 'error' field
125 | -}
126 | errorDecoder : (String -> a) -> Json.Decoder a
127 | errorDecoder errorCodeFromString =
128 | Json.map errorCodeFromString <| Json.field "error" Json.string
129 |
130 |
131 | {-| Json decoder for 'error\_description' field
132 | -}
133 | errorDescriptionDecoder : Json.Decoder (Maybe String)
134 | errorDescriptionDecoder =
135 | Json.maybe <| Json.field "error_description" Json.string
136 |
137 |
138 | {-| Json decoder for 'error\_uri' field
139 | -}
140 | errorUriDecoder : Json.Decoder (Maybe String)
141 | errorUriDecoder =
142 | Json.maybe <| Json.field "error_uri" Json.string
143 |
144 |
145 | {-| Combinator for JSON decoders to extract values from a `Maybe` or fail
146 | with the given message (when `Nothing` is encountered)
147 | -}
148 | decoderFromJust : String -> Maybe a -> Json.Decoder a
149 | decoderFromJust msg =
150 | Maybe.map Json.succeed >> Maybe.withDefault (Json.fail msg)
151 |
152 |
153 | {-| Combinator for JSON decoders to extact values from a `Result _ _` or fail
154 | with an appropriate message
155 | -}
156 | decoderFromResult : Result String a -> Json.Decoder a
157 | decoderFromResult res =
158 | case res of
159 | Err msg ->
160 | Json.fail msg
161 |
162 | Ok a ->
163 | Json.succeed a
164 |
165 |
166 |
167 | --
168 | -- Query Parsers
169 | --
170 |
171 |
172 | authorizationErrorParser : e -> Query.Parser (AuthorizationError e)
173 | authorizationErrorParser errorCode =
174 | Query.map3 (AuthorizationError errorCode)
175 | errorDescriptionParser
176 | errorUriParser
177 | stateParser
178 |
179 |
180 | tokenParser : Query.Parser (Maybe Token)
181 | tokenParser =
182 | Query.map2 makeToken
183 | (Query.string "token_type")
184 | (Query.string "access_token")
185 |
186 |
187 | errorParser : (String -> e) -> Query.Parser (Maybe e)
188 | errorParser errorCodeFromString =
189 | Query.map (Maybe.map errorCodeFromString)
190 | (Query.string "error")
191 |
192 |
193 | expiresInParser : Query.Parser (Maybe Int)
194 | expiresInParser =
195 | Query.int "expires_in"
196 |
197 |
198 | scopeParser : Query.Parser (List String)
199 | scopeParser =
200 | spaceSeparatedListParser "scope"
201 |
202 |
203 | stateParser : Query.Parser (Maybe String)
204 | stateParser =
205 | Query.string "state"
206 |
207 |
208 | errorDescriptionParser : Query.Parser (Maybe String)
209 | errorDescriptionParser =
210 | Query.string "error_description"
211 |
212 |
213 | errorUriParser : Query.Parser (Maybe String)
214 | errorUriParser =
215 | Query.string "error_uri"
216 |
217 |
218 | spaceSeparatedListParser : String -> Query.Parser (List String)
219 | spaceSeparatedListParser param =
220 | Query.map
221 | (\s ->
222 | case s of
223 | Nothing ->
224 | []
225 |
226 | Just str ->
227 | String.split " " str
228 | )
229 | (Query.string param)
230 |
231 |
232 | urlAddList : String -> List String -> List QueryParameter -> List QueryParameter
233 | urlAddList param xs qs =
234 | qs
235 | ++ (case xs of
236 | [] ->
237 | []
238 |
239 | _ ->
240 | [ Builder.string param (String.join " " xs) ]
241 | )
242 |
243 |
244 | urlAddMaybe : String -> Maybe String -> List QueryParameter -> List QueryParameter
245 | urlAddMaybe param ms qs =
246 | qs
247 | ++ (case ms of
248 | Nothing ->
249 | []
250 |
251 | Just s ->
252 | [ Builder.string param s ]
253 | )
254 |
255 |
256 | urlAddExtraFields : Dict String String -> List QueryParameter -> List QueryParameter
257 | urlAddExtraFields extraFields zero =
258 | Dict.foldr (\k v qs -> Builder.string k v :: qs) zero extraFields
259 |
260 |
261 |
262 | --
263 | -- Smart Constructors
264 | --
265 |
266 |
267 | makeAuthorizationUrl : ResponseType -> Dict String String -> Authorization -> Url
268 | makeAuthorizationUrl responseType extraFields { clientId, url, redirectUri, scope, state } =
269 | let
270 | query =
271 | [ Builder.string "client_id" clientId
272 | , Builder.string "redirect_uri" (makeRedirectUri redirectUri)
273 | , Builder.string "response_type" (responseTypeToString responseType)
274 | ]
275 | |> urlAddList "scope" scope
276 | |> urlAddMaybe "state" state
277 | |> urlAddExtraFields extraFields
278 | |> Builder.toQuery
279 | |> String.dropLeft 1
280 | in
281 | case url.query of
282 | Nothing ->
283 | { url | query = Just query }
284 |
285 | Just baseQuery ->
286 | { url | query = Just (baseQuery ++ "&" ++ query) }
287 |
288 |
289 | makeRequest : Json.Decoder success -> (Result Http.Error success -> msg) -> Url -> List Http.Header -> String -> RequestParts msg
290 | makeRequest decoder toMsg url headers body =
291 | { method = "POST"
292 | , headers = headers
293 | , url = Url.toString url
294 | , body = Http.stringBody "application/x-www-form-urlencoded" body
295 | , expect = Http.expectJson toMsg decoder
296 | , timeout = Nothing
297 | , tracker = Nothing
298 | }
299 |
300 |
301 | makeHeaders : Maybe { clientId : String, secret : String } -> List Http.Header
302 | makeHeaders credentials =
303 | credentials
304 | |> Maybe.map (\{ clientId, secret } -> Base64.encode <| Base64.string <| (clientId ++ ":" ++ secret))
305 | |> Maybe.map (\s -> [ Http.header "Authorization" ("Basic " ++ s) ])
306 | |> Maybe.withDefault []
307 |
308 |
309 | makeRedirectUri : Url -> String
310 | makeRedirectUri url =
311 | String.concat
312 | [ protocolToString url.protocol
313 | , "://"
314 | , url.host
315 | , Maybe.withDefault "" (Maybe.map (\i -> ":" ++ String.fromInt i) url.port_)
316 | , url.path
317 | , Maybe.withDefault "" (Maybe.map (\q -> "?" ++ q) url.query)
318 | ]
319 |
320 |
321 |
322 | --
323 | -- String utilities
324 | --
325 |
326 |
327 | {-| Gets the `String` representation of an `Protocol`
328 | -}
329 | protocolToString : Protocol -> String
330 | protocolToString protocol =
331 | case protocol of
332 | Http ->
333 | "http"
334 |
335 | Https ->
336 | "https"
337 |
338 |
339 |
340 | --
341 | -- Utils
342 | --
343 |
344 |
345 | parseUrlQuery : Url -> a -> Query.Parser a -> a
346 | parseUrlQuery url def parser =
347 | Maybe.withDefault def <| Url.parse (Url.query parser) url
348 |
349 |
350 | {-| Extracts the intrinsic value of a `Token`. Careful with this, we don't have
351 | access to the `Token` constructors, so it's a bit Houwje-Touwje
352 | -}
353 | extractTokenString : Token -> String
354 | extractTokenString =
355 | tokenToString >> String.dropLeft 7
356 |
357 |
358 |
359 | --
360 | -- Record Alias Re-Definition
361 | --
362 |
363 |
364 | type alias RequestParts a =
365 | { method : String
366 | , headers : List Http.Header
367 | , url : String
368 | , body : Http.Body
369 | , expect : Http.Expect a
370 | , timeout : Maybe Float
371 | , tracker : Maybe String
372 | }
373 |
374 |
375 | type alias Authorization =
376 | { clientId : String
377 | , url : Url
378 | , redirectUri : Url
379 | , scope : List String
380 | , state : Maybe String
381 | }
382 |
383 |
384 | type alias AuthorizationError e =
385 | { error : e
386 | , errorDescription : Maybe String
387 | , errorUri : Maybe String
388 | , state : Maybe String
389 | }
390 |
391 |
392 | type alias AuthenticationSuccess =
393 | { token : Token
394 | , refreshToken : Maybe Token
395 | , expiresIn : Maybe Int
396 | , scope : List String
397 | }
398 |
399 |
400 | type alias AuthenticationError e =
401 | { error : e
402 | , errorDescription : Maybe String
403 | , errorUri : Maybe String
404 | }
405 |
--------------------------------------------------------------------------------
/src/OAuth.elm:
--------------------------------------------------------------------------------
1 | module OAuth exposing
2 | ( Token, useToken, tokenToString, tokenFromString
3 | , ErrorCode(..), errorCodeToString, errorCodeFromString
4 | , ResponseType(..), responseTypeToString, GrantType(..), grantTypeToString
5 | , TokenType, TokenString, makeToken, makeRefreshToken
6 | )
7 |
8 | {-| Utility library to manage client-side OAuth 2.0 authentications
9 |
10 | The library contains a main OAuth module exposing types used accross other modules. In practice,
11 | you'll only need to use one of the additional modules:
12 |
13 | - OAuth.AuthorizationCode: The authorization code grant type is used to obtain both access tokens
14 | and refresh tokens via a redirection-based flow and is optimized for confidential clients
15 | [4.1](https://tools.ietf.org/html/rfc6749#section-4.1).
16 |
17 | - OAuth.AuthorizationCode.PKCE: An extension of the original OAuth 2.0 specification to mitigate
18 | authorization code interception attacks through the use of Proof Key for Code Exchange (PKCE).
19 |
20 | - OAuth.Implicit: The implicit grant type is used to obtain access tokens (it does not support the
21 | issuance of refresh tokens) and is optimized for public clients known to operate a particular
22 | redirection URI [4.2](https://tools.ietf.org/html/rfc6749#section-4.2).
23 |
24 | - OAuth.Password: The resource owner password credentials grant type is suitable in cases where the
25 | resource owner has a trust relationship with the client, such as the device operating system or a
26 | highly privileged application [4.3](https://tools.ietf.org/html/rfc6749#section-4.3)
27 |
28 | - OAuth.ClientCredentials: The client can request an access token using only its client credentials
29 | (or other supported means of authentication) when the client is requesting access to the protected
30 | resources under its control, or those of another resource owner that have been previously arranged
31 | with the authorization server (the method of which is beyond the scope of this specification)
32 | [4.4](https://tools.ietf.org/html/rfc6749#section-4.3).
33 |
34 | In practice, you most probably want to use the
35 | [`OAuth.AuthorizationCode`](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest/OAuth-AuthorizationCode).
36 | If your authorization server supports it, you should look at the PKCE extension in a second-time!
37 |
38 | which is the most commonly
39 | used.
40 |
41 |
42 | ## Token
43 |
44 | @docs Token, useToken, tokenToString, tokenFromString
45 |
46 |
47 | ## ErrorCode
48 |
49 | @docs ErrorCode, errorCodeToString, errorCodeFromString
50 |
51 |
52 | ## Response & Grant types (Advanced)
53 |
54 | The following section can be ignored if you're dealing with a very generic OAuth2.0 implementation. If however, your authorization server does implement some extra features on top of the OAuth2.0 protocol (e.g. OpenID Connect), you will require to tweak response parsers and possibly, response type to cope with these discrepancies. In short, unless you're planning on using `makeTokenRequestWith` or `makeAuthorizationUrlWith`, you most probably won't need any of the functions below.
55 |
56 | @docs ResponseType, responseTypeToString, GrantType, grantTypeToString
57 |
58 |
59 | ## Decoders & Parsers Utils (advanced)
60 |
61 | @docs TokenType, TokenString, makeToken, makeRefreshToken
62 |
63 | -}
64 |
65 | import Extra.Maybe as Maybe
66 | import Http as Http
67 |
68 |
69 |
70 | --
71 | -- Token
72 | --
73 |
74 |
75 | {-| Describes the type of access token to use.
76 |
77 | - Bearer: Utilized by simply including the access token string in the request
78 | [rfc6750](https://tools.ietf.org/html/rfc6750)
79 |
80 | - Mac: Not supported.
81 |
82 | -}
83 | type Token
84 | = Bearer String
85 |
86 |
87 | {-| Alias for readability
88 | -}
89 | type alias TokenType =
90 | String
91 |
92 |
93 | {-| Alias for readability
94 | -}
95 | type alias TokenString =
96 | String
97 |
98 |
99 | {-| Use a token to authenticate a request.
100 | -}
101 | useToken : Token -> List Http.Header -> List Http.Header
102 | useToken token =
103 | (::) (Http.header "Authorization" (tokenToString token))
104 |
105 |
106 | {-| Create a token from two string representing a token type and
107 | an actual token value. This is intended to be used in Json decoders
108 | or Query parsers.
109 |
110 | Returns `Nothing` when the token type is `Nothing`
111 | , different from `Just "Bearer"` or when there's no token at all.
112 |
113 | -}
114 | makeToken : Maybe TokenType -> Maybe TokenString -> Maybe Token
115 | makeToken =
116 | Maybe.andThen2 tryMakeToken
117 |
118 |
119 | {-| See `makeToken`, with the subtle difference that a token value may or
120 | may not be there.
121 |
122 | Returns `Nothing` when the token type isn't `"Bearer"`.
123 |
124 | Returns `Just Nothing` or `Just (Just token)` otherwise, depending on whether a token is
125 | present or not.
126 |
127 | -}
128 | makeRefreshToken : TokenType -> Maybe TokenString -> Maybe (Maybe Token)
129 | makeRefreshToken tokenType mToken =
130 | case ( mToken, Maybe.andThen2 tryMakeToken (Just tokenType) mToken ) of
131 | ( Nothing, _ ) ->
132 | Just Nothing
133 |
134 | ( _, Just token ) ->
135 | Just <| Just token
136 |
137 | _ ->
138 | Nothing
139 |
140 |
141 | {-| Internal, attempt to make a Bearer token from a type and a token string
142 | -}
143 | tryMakeToken : TokenType -> TokenString -> Maybe Token
144 | tryMakeToken tokenType token =
145 | case String.toLower tokenType of
146 | "bearer" ->
147 | Just (Bearer token)
148 |
149 | _ ->
150 | Nothing
151 |
152 |
153 | {-| Get the `String` representation of a `Token` to be used in an 'Authorization' header
154 | -}
155 | tokenToString : Token -> String
156 | tokenToString (Bearer t) =
157 | "Bearer " ++ t
158 |
159 |
160 | {-| Parse a token from an 'Authorization' header string.
161 |
162 | tokenFromString (tokenToString token) == Just token
163 |
164 | -}
165 | tokenFromString : String -> Maybe Token
166 | tokenFromString str =
167 | case ( String.left 6 str, String.dropLeft 7 str ) of
168 | ( "Bearer", t ) ->
169 | Just (Bearer t)
170 |
171 | _ ->
172 | Nothing
173 |
174 |
175 |
176 | --
177 | -- ResponseType / GrandType
178 | --
179 |
180 |
181 | {-| Describes the desired type of response to an authorization. Use `Code` to ask for an
182 | authorization code and continue with the according flow. Use `Token` to do an implicit
183 | authentication and directly retrieve a `Token` from the authorization. If need be, you may provide a
184 | custom response type should the server returns a non-standard response type.
185 | -}
186 | type ResponseType
187 | = Code
188 | | Token
189 | | CustomResponse String
190 |
191 |
192 | {-| Gets the `String` representation of a `ResponseType`.
193 | -}
194 | responseTypeToString : ResponseType -> String
195 | responseTypeToString r =
196 | case r of
197 | Code ->
198 | "code"
199 |
200 | Token ->
201 | "token"
202 |
203 | CustomResponse str ->
204 | str
205 |
206 |
207 | {-| Describes the desired type of grant to an authentication.
208 | -}
209 | type GrantType
210 | = AuthorizationCode
211 | | Password
212 | | ClientCredentials
213 | | RefreshToken
214 | | CustomGrant String
215 |
216 |
217 | {-| Gets the `String` representation of a `GrantType`
218 | -}
219 | grantTypeToString : GrantType -> String
220 | grantTypeToString g =
221 | case g of
222 | AuthorizationCode ->
223 | "authorization_code"
224 |
225 | Password ->
226 | "password"
227 |
228 | ClientCredentials ->
229 | "client_credentials"
230 |
231 | RefreshToken ->
232 | "refresh_token"
233 |
234 | CustomGrant str ->
235 | str
236 |
237 |
238 |
239 | --
240 | -- Error
241 | --
242 |
243 |
244 | {-| Describes an OAuth error response [4.1.2.1](https://tools.ietf.org/html/rfc6749#section-4.1.2.1)
245 |
246 | - `InvalidRequest`: The request is missing a required parameter, includes an invalid parameter value,
247 | includes a parameter more than once, or is otherwise malformed.
248 |
249 | - `UnauthorizedClient`: The client is not authorized to request an authorization code using this
250 | method.
251 |
252 | - `AccessDenied`: The resource owner or authorization server denied the request.
253 |
254 | - `UnsupportedResponseType`: The authorization server does not support obtaining an authorization code
255 | using this method.
256 |
257 | - `InvalidScope`: The requested scope is invalid, unknown, or malformed.
258 |
259 | - `ServerError`: The authorization server encountered an unexpected condition that prevented it from
260 | fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status
261 | code cannot be returned to the client via an HTTP redirect.)
262 |
263 | - `TemporarilyUnavailable`: The authorization server is currently unable to handle the request due to
264 | a temporary overloading or maintenance of the server. (This error code is needed because a 503
265 | Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)
266 |
267 | - `Custom`: Encountered a 'free-string' or custom code not specified by the official RFC but returned
268 | by the authorization server.
269 |
270 | -}
271 | type ErrorCode
272 | = InvalidRequest
273 | | UnauthorizedClient
274 | | AccessDenied
275 | | UnsupportedResponseType
276 | | InvalidScope
277 | | ServerError
278 | | TemporarilyUnavailable
279 | | Custom String
280 |
281 |
282 | {-| Get the `String` representation of an `ErrorCode`.
283 | -}
284 | errorCodeToString : ErrorCode -> String
285 | errorCodeToString err =
286 | case err of
287 | InvalidRequest ->
288 | "invalid_request"
289 |
290 | UnauthorizedClient ->
291 | "unauthorized_client"
292 |
293 | AccessDenied ->
294 | "access_denied"
295 |
296 | UnsupportedResponseType ->
297 | "unsupported_response_type"
298 |
299 | InvalidScope ->
300 | "invalid_scope"
301 |
302 | ServerError ->
303 | "server_error"
304 |
305 | TemporarilyUnavailable ->
306 | "temporarily_unavailable"
307 |
308 | Custom str ->
309 | str
310 |
311 |
312 | {-| Build a string back into an error code. Returns `Custom _`
313 | when the string isn't recognized from the ones specified in the RFC
314 | -}
315 | errorCodeFromString : String -> ErrorCode
316 | errorCodeFromString str =
317 | case str of
318 | "invalid_request" ->
319 | InvalidRequest
320 |
321 | "unauthorized_client" ->
322 | UnauthorizedClient
323 |
324 | "access_denied" ->
325 | AccessDenied
326 |
327 | "unsupported_response_type" ->
328 | UnsupportedResponseType
329 |
330 | "invalid_scope" ->
331 | InvalidScope
332 |
333 | "server_error" ->
334 | ServerError
335 |
336 | "temporarily_unavailable" ->
337 | TemporarilyUnavailable
338 |
339 | _ ->
340 | Custom str
341 |
--------------------------------------------------------------------------------
/src/OAuth/ClientCredentials.elm:
--------------------------------------------------------------------------------
1 | module OAuth.ClientCredentials exposing
2 | ( makeTokenRequest, Authentication, Credentials, AuthenticationSuccess, AuthenticationError, RequestParts
3 | , defaultAuthenticationSuccessDecoder, defaultAuthenticationErrorDecoder
4 | , makeTokenRequestWith, defaultExpiresInDecoder, defaultScopeDecoder, lenientScopeDecoder, defaultTokenDecoder, defaultRefreshTokenDecoder, defaultErrorDecoder, defaultErrorDescriptionDecoder, defaultErrorUriDecoder
5 | )
6 |
7 | {-| The client can request an access token using only its client
8 | credentials (or other supported means of authentication) when the client is requesting access to
9 | the protected resources under its control, or those of another resource owner that have been
10 | previously arranged with the authorization server (the method of which is beyond the scope of
11 | this specification).
12 |
13 | There's only one step in this process:
14 |
15 | - The client authenticates itself directly using credentials it owns.
16 |
17 | After this step, the client owns a `Token` that can be used to authorize any subsequent
18 | request.
19 |
20 |
21 | ## Authenticate
22 |
23 | @docs makeTokenRequest, Authentication, Credentials, AuthenticationSuccess, AuthenticationError, RequestParts
24 |
25 |
26 | ## JSON Decoders
27 |
28 | @docs defaultAuthenticationSuccessDecoder, defaultAuthenticationErrorDecoder
29 |
30 |
31 | ## Custom Decoders & Parsers (advanced)
32 |
33 | @docs makeTokenRequestWith, defaultExpiresInDecoder, defaultScopeDecoder, lenientScopeDecoder, defaultTokenDecoder, defaultRefreshTokenDecoder, defaultErrorDecoder, defaultErrorDescriptionDecoder, defaultErrorUriDecoder
34 |
35 | -}
36 |
37 | import Dict as Dict exposing (Dict)
38 | import Http
39 | import Internal as Internal exposing (..)
40 | import Json.Decode as Json
41 | import OAuth exposing (ErrorCode(..), GrantType(..), Token, errorCodeFromString, grantTypeToString)
42 | import Url exposing (Url)
43 | import Url.Builder as Builder
44 |
45 |
46 |
47 | --
48 | -- Authenticate
49 | --
50 |
51 |
52 | {-| Request configuration for a ClientCredentials authentication
53 |
54 | - `credentials` (_REQUIRED_):
55 | Credentials needed for Basic authentication.
56 |
57 | - `url` (_REQUIRED_):
58 | The token endpoint to contact the authorization server.
59 |
60 | - `scope` (_OPTIONAL_):
61 | The scope of the access request.
62 |
63 | -}
64 | type alias Authentication =
65 | { credentials : Credentials
66 | , url : Url
67 | , scope : List String
68 | }
69 |
70 |
71 | {-| Describes a couple of client credentials used for Basic authentication
72 |
73 | { clientId = ""
74 | , secret = ""
75 | }
76 |
77 | -}
78 | type alias Credentials =
79 | { clientId : String, secret : String }
80 |
81 |
82 | {-| The response obtained as a result of an authentication (implicit or not)
83 |
84 | - `token` (_REQUIRED_):
85 | The access token issued by the authorization server.
86 |
87 | - `refreshToken` (_OPTIONAL_):
88 | The refresh token, which can be used to obtain new access tokens using the same authorization
89 | grant as described in [Section 6](https://tools.ietf.org/html/rfc6749#section-6).
90 |
91 | - `expiresIn` (_RECOMMENDED_):
92 | The lifetime in seconds of the access token. For example, the value "3600" denotes that the
93 | access token will expire in one hour from the time the response was generated. If omitted, the
94 | authorization server SHOULD provide the expiration time via other means or document the default
95 | value.
96 |
97 | - `scope` (_OPTIONAL, if identical to the scope requested; otherwise, REQUIRED_):
98 | The scope of the access token as described by [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3).
99 |
100 | -}
101 | type alias AuthenticationSuccess =
102 | { token : Token
103 | , refreshToken : Maybe Token
104 | , expiresIn : Maybe Int
105 | , scope : List String
106 | }
107 |
108 |
109 | {-| Describes an OAuth error as a result of a request failure
110 |
111 | - `error` (_REQUIRED_):
112 | A single ASCII error code.
113 |
114 | - `errorDescription` (_OPTIONAL_)
115 | Human-readable ASCII text providing additional information, used to assist the client developer
116 | in understanding the error that occurred. Values for the `errorDescription` parameter MUST NOT
117 | include characters outside the set `%x20-21 / %x23-5B / %x5D-7E`.
118 |
119 | - `errorUri` (_OPTIONAL_):
120 | A URI identifying a human-readable web page with information about the error, used to provide
121 | the client developer with additional information about the error. Values for the `errorUri`
122 | parameter MUST conform to the URI-reference syntax and thus MUST NOT include characters outside
123 | the set `%x21 / %x23-5B / %x5D-7E`.
124 |
125 | -}
126 | type alias AuthenticationError =
127 | { error : ErrorCode
128 | , errorDescription : Maybe String
129 | , errorUri : Maybe String
130 | }
131 |
132 |
133 | {-| Parts required to build a request. This record is given to [`Http.request`](https://package.elm-lang.org/packages/elm/http/latest/Http#request)
134 | in order to create a new request and may be adjusted at will.
135 | -}
136 | type alias RequestParts a =
137 | { method : String
138 | , headers : List Http.Header
139 | , url : String
140 | , body : Http.Body
141 | , expect : Http.Expect a
142 | , timeout : Maybe Float
143 | , tracker : Maybe String
144 | }
145 |
146 |
147 | {-| Builds a the request components required to get a token from client credentials
148 |
149 | let req : Http.Request TokenResponse
150 | req = makeTokenRequest toMsg authentication |> Http.request
151 |
152 | -}
153 | makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg
154 | makeTokenRequest =
155 | makeTokenRequestWith ClientCredentials defaultAuthenticationSuccessDecoder Dict.empty
156 |
157 |
158 |
159 | --
160 | -- Json Decoders
161 | --
162 |
163 |
164 | {-| Json decoder for a positive response. You may provide a custom response decoder using other decoders
165 | from this module, or some of your own craft.
166 |
167 | defaultAuthenticationSuccessDecoder : Decoder AuthenticationSuccess
168 | defaultAuthenticationSuccessDecoder =
169 | D.map4 AuthenticationSuccess
170 | tokenDecoder
171 | refreshTokenDecoder
172 | expiresInDecoder
173 | scopeDecoder
174 |
175 | -}
176 | defaultAuthenticationSuccessDecoder : Json.Decoder AuthenticationSuccess
177 | defaultAuthenticationSuccessDecoder =
178 | Internal.authenticationSuccessDecoder
179 |
180 |
181 | {-| Json decoder for an errored response.
182 |
183 | case res of
184 | Err (Http.BadStatus { body }) ->
185 | case Json.decodeString OAuth.ClientCredentials.defaultAuthenticationErrorDecoder body of
186 | Ok { error, errorDescription } ->
187 | doSomething
188 |
189 | _ ->
190 | parserFailed
191 |
192 | _ ->
193 | someOtherError
194 |
195 | -}
196 | defaultAuthenticationErrorDecoder : Json.Decoder AuthenticationError
197 | defaultAuthenticationErrorDecoder =
198 | Internal.authenticationErrorDecoder defaultErrorDecoder
199 |
200 |
201 |
202 | --
203 | -- Custom Decoders & Parsers (advanced)
204 | --
205 |
206 |
207 | {-| Like [`makeTokenRequest`](#makeTokenRequest), but gives you the ability to specify custom grant
208 | type and extra fields to be set on the query.
209 |
210 | makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg
211 | makeTokenRequest =
212 | makeTokenRequestWith ClientCredentials defaultAuthenticationSuccessDecoder Dict.empty
213 |
214 | -}
215 | makeTokenRequestWith : GrantType -> Json.Decoder success -> Dict String String -> (Result Http.Error success -> msg) -> Authentication -> RequestParts msg
216 | makeTokenRequestWith grantType decoder extraFields toMsg { credentials, scope, url } =
217 | let
218 | body =
219 | [ Builder.string "grant_type" (grantTypeToString grantType) ]
220 | |> urlAddList "scope" scope
221 | |> urlAddExtraFields extraFields
222 | |> Builder.toQuery
223 | |> String.dropLeft 1
224 |
225 | headers =
226 | makeHeaders <|
227 | Just
228 | { clientId = credentials.clientId
229 | , secret = credentials.secret
230 | }
231 | in
232 | makeRequest decoder toMsg url headers body
233 |
234 |
235 | {-| Json decoder for the `expiresIn` field.
236 | -}
237 | defaultExpiresInDecoder : Json.Decoder (Maybe Int)
238 | defaultExpiresInDecoder =
239 | Internal.expiresInDecoder
240 |
241 |
242 | {-| Json decoder for the `scope` field (space-separated).
243 | -}
244 | defaultScopeDecoder : Json.Decoder (List String)
245 | defaultScopeDecoder =
246 | Internal.scopeDecoder
247 |
248 |
249 | {-| Json decoder for the `scope` field (comma- or space-separated).
250 | -}
251 | lenientScopeDecoder : Json.Decoder (List String)
252 | lenientScopeDecoder =
253 | Internal.lenientScopeDecoder
254 |
255 |
256 | {-| Json decoder for the `access_token` field.
257 | -}
258 | defaultTokenDecoder : Json.Decoder Token
259 | defaultTokenDecoder =
260 | Internal.tokenDecoder
261 |
262 |
263 | {-| Json decoder for the `refresh_token` field.
264 | -}
265 | defaultRefreshTokenDecoder : Json.Decoder (Maybe Token)
266 | defaultRefreshTokenDecoder =
267 | Internal.refreshTokenDecoder
268 |
269 |
270 | {-| Json decoder for the `error` field.
271 | -}
272 | defaultErrorDecoder : Json.Decoder ErrorCode
273 | defaultErrorDecoder =
274 | Internal.errorDecoder errorCodeFromString
275 |
276 |
277 | {-| Json decoder for the `error_description` field.
278 | -}
279 | defaultErrorDescriptionDecoder : Json.Decoder (Maybe String)
280 | defaultErrorDescriptionDecoder =
281 | Internal.errorDescriptionDecoder
282 |
283 |
284 | {-| Json decoder for the `error_uri` field.
285 | -}
286 | defaultErrorUriDecoder : Json.Decoder (Maybe String)
287 | defaultErrorUriDecoder =
288 | Internal.errorUriDecoder
289 |
--------------------------------------------------------------------------------
/src/OAuth/Implicit.elm:
--------------------------------------------------------------------------------
1 | module OAuth.Implicit exposing
2 | ( makeAuthorizationUrl, Authorization, parseToken, AuthorizationResult, AuthorizationResultWith(..), AuthorizationError, AuthorizationSuccess
3 | , makeAuthorizationUrlWith, parseTokenWith, Parsers, defaultParsers, defaultTokenParser, defaultErrorParser, defaultAuthorizationSuccessParser, defaultAuthorizationErrorParser
4 | )
5 |
6 | {-| **⚠ (DEPRECATED) ⚠ You should probably look into [OAuth.AuthorizationCode](http://package.elm-lang.org/packages/truqu/elm-oauth2/latest/OAuth-AuthorizationCode) instead.**
7 |
8 | The implicit grant type is used to obtain access tokens (it does not
9 | support the issuance of refresh tokens) and is optimized for public clients known to operate a
10 | particular redirection URI. These clients are typically implemented in a browser using a
11 | scripting language such as JavaScript.
12 |
13 |
14 | ## Quick Start
15 |
16 | To get started, have a look at the [live-demo](https://truqu.github.io/elm-oauth2/auth0/implicit/) and its
17 | corresponding [source
18 | code](https://github.com/truqu/elm-oauth2/blob/master/examples/providers/auth0/implicit/Main.elm).
19 |
20 |
21 | ## Overview
22 |
23 | +---------+ +--------+
24 | | |---(A)- Auth Redirection ------>| |
25 | | | | Auth |
26 | | Browser | | Server |
27 | | | | |
28 | | |<--(B)- Redirection Callback ---| |
29 | +---------+ w/ Access Token +--------+
30 | ^ |
31 | | |
32 | (A) (B)
33 | | |
34 | | v
35 | +---------+
36 | | |
37 | | Elm App |
38 | | |
39 | | |
40 | +---------+
41 |
42 | After those steps, the client owns a `Token` that can be used to authorize any subsequent
43 | request.
44 |
45 |
46 | ## Authorize
47 |
48 | @docs makeAuthorizationUrl, Authorization, parseToken, AuthorizationResult, AuthorizationResultWith, AuthorizationError, AuthorizationSuccess
49 |
50 |
51 | ## Custom Parsers (advanced)
52 |
53 | @docs makeAuthorizationUrlWith, parseTokenWith, Parsers, defaultParsers, defaultTokenParser, defaultErrorParser, defaultAuthorizationSuccessParser, defaultAuthorizationErrorParser
54 |
55 | -}
56 |
57 | import Dict as Dict exposing (Dict)
58 | import Internal exposing (..)
59 | import OAuth exposing (ErrorCode(..), ResponseType(..), Token, errorCodeFromString)
60 | import Url exposing (Protocol(..), Url)
61 | import Url.Parser as Url exposing ((>))
62 | import Url.Parser.Query as Query
63 |
64 |
65 |
66 | --
67 | -- Authorize
68 | --
69 |
70 |
71 | {-| Request configuration for an authorization
72 |
73 | - `clientId` (_REQUIRED_):
74 | The client identifier issues by the authorization server via an off-band mechanism.
75 |
76 | - `url` (_REQUIRED_):
77 | The authorization endpoint to contact the authorization server.
78 |
79 | - `redirectUri` (_OPTIONAL_):
80 | After completing its interaction with the resource owner, the authorization
81 | server directs the resource owner's user-agent back to the client via this
82 | URL. May be already defined on the authorization server itself.
83 |
84 | - `scope` (_OPTIONAL_):
85 | The scope of the access request.
86 |
87 | - `state` (_RECOMMENDED_):
88 | An opaque value used by the client to maintain state between the request
89 | and callback. The authorization server includes this value when redirecting
90 | the user-agent back to the client. The parameter SHOULD be used for preventing
91 | cross-site request forgery.
92 |
93 | -}
94 | type alias Authorization =
95 | { clientId : String
96 | , url : Url
97 | , redirectUri : Url
98 | , scope : List String
99 | , state : Maybe String
100 | }
101 |
102 |
103 | {-| Describes an OAuth error as a result of an authorization request failure
104 |
105 | - `error` (_REQUIRED_):
106 | A single ASCII error code.
107 |
108 | - `errorDescription` (_OPTIONAL_)
109 | Human-readable ASCII text providing additional information, used to assist the client developer in
110 | understanding the error that occurred. Values for the `errorDescription` parameter MUST NOT
111 | include characters outside the set `%x20-21 / %x23-5B / %x5D-7E`.
112 |
113 | - `errorUri` (_OPTIONAL_):
114 | A URI identifying a human-readable web page with information about the error, used to
115 | provide the client developer with additional information about the error. Values for the
116 | `errorUri` parameter MUST conform to the URI-reference syntax and thus MUST NOT include
117 | characters outside the set `%x21 / %x23-5B / %x5D-7E`.
118 |
119 | - `state` (_REQUIRED if `state` was present in the authorization request_):
120 | The exact value received from the client
121 |
122 | -}
123 | type alias AuthorizationError =
124 | { error : ErrorCode
125 | , errorDescription : Maybe String
126 | , errorUri : Maybe String
127 | , state : Maybe String
128 | }
129 |
130 |
131 | {-| The response obtained as a result of an authentication (implicit or not)
132 |
133 | - `token` (_REQUIRED_):
134 | The access token issued by the authorization server.
135 |
136 | - `refreshToken` (_OPTIONAL_):
137 | The refresh token, which can be used to obtain new access tokens using the same authorization
138 | grant as described in [Section 6](https://tools.ietf.org/html/rfc6749#section-6).
139 |
140 | - `expiresIn` (_RECOMMENDED_):
141 | The lifetime in seconds of the access token. For example, the value "3600" denotes that the
142 | access token will expire in one hour from the time the response was generated. If omitted, the
143 | authorization server SHOULD provide the expiration time via other means or document the default
144 | value.
145 |
146 | - `scope` (_OPTIONAL, if identical to the scope requested; otherwise, REQUIRED_):
147 | The scope of the access token as described by [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3).
148 |
149 | - `state` (_REQUIRED if `state` was present in the authorization request_):
150 | The exact value received from the client
151 |
152 | -}
153 | type alias AuthorizationSuccess =
154 | { token : Token
155 | , refreshToken : Maybe Token
156 | , expiresIn : Maybe Int
157 | , scope : List String
158 | , state : Maybe String
159 | }
160 |
161 |
162 | {-| Describes errors coming from attempting to parse a url after an OAuth redirection.
163 | -}
164 | type alias AuthorizationResult =
165 | AuthorizationResultWith AuthorizationError AuthorizationSuccess
166 |
167 |
168 | {-| A parameterized [`AuthorizationResult`](#AuthorizationResult), see [`parseTokenWith`](#parseTokenWith).
169 |
170 | - `Empty`: means there were nothing (related to OAuth 2.0) to parse
171 | - `Error`: a successfully parsed OAuth 2.0 error
172 | - `Success`: a successfully parsed token and response
173 |
174 | -}
175 | type AuthorizationResultWith error success
176 | = Empty
177 | | Error error
178 | | Success success
179 |
180 |
181 | {-| Redirects the resource owner (user) to the resource provider server using the specified
182 | authorization flow.
183 | -}
184 | makeAuthorizationUrl : Authorization -> Url
185 | makeAuthorizationUrl =
186 | makeAuthorizationUrlWith Token Dict.empty
187 |
188 |
189 | {-| Parses the location looking for parameters in the fragment set by the
190 | authorization server after redirecting the resource owner (user).
191 |
192 | Returns `ParseResult Empty` when there's nothing or an invalid Url is passed
193 |
194 | -}
195 | parseToken : Url -> AuthorizationResult
196 | parseToken =
197 | parseTokenWith defaultParsers
198 |
199 |
200 |
201 | --
202 | -- Custom Parsers (Advanced)
203 | --
204 |
205 |
206 | {-| Like [`makeAuthorizationUrl`](#makeAuthorizationUrl), but gives you the ability to specify a
207 | custom response type and extra fields to be set on the query.
208 |
209 | makeAuthorizationUrl : Authorization -> Url
210 | makeAuthorizationUrl =
211 | makeAuthorizationUrlWith Token Dict.empty
212 |
213 | For example, to interact with a service implementing `OpenID+Connect` you may require a different
214 | token type and an extra query parameter as such:
215 |
216 | makeAuthorizationUrlWith
217 | (CustomResponse "token+id_token")
218 | (Dict.fromList [ ( "resource", "001" ) ])
219 | authorization
220 |
221 | -}
222 | makeAuthorizationUrlWith : ResponseType -> Dict String String -> Authorization -> Url
223 | makeAuthorizationUrlWith responseType extraFields { clientId, url, redirectUri, scope, state } =
224 | Internal.makeAuthorizationUrl
225 | responseType
226 | extraFields
227 | { clientId = clientId
228 | , url = url
229 | , redirectUri = redirectUri
230 | , scope = scope
231 | , state = state
232 | }
233 |
234 |
235 | {-| Like [`parseToken`](#parseToken), but gives you the ability to provide your own custom parsers.
236 |
237 | This is especially useful when interacting with authorization servers that don't quite
238 | implement the OAuth2.0 specifications.
239 |
240 | parseToken : Url -> AuthorizationResultWith AuthorizationError AuthorizationSuccess
241 | parseToken =
242 | parseTokenWith defaultParsers
243 |
244 | -}
245 | parseTokenWith : Parsers error success -> Url -> AuthorizationResultWith error success
246 | parseTokenWith { tokenParser, errorParser, authorizationSuccessParser, authorizationErrorParser } url_ =
247 | let
248 | url =
249 | { url_ | path = "/", query = url_.fragment, fragment = Nothing }
250 | in
251 | case Url.parse (Url.top > Query.map2 Tuple.pair tokenParser errorParser) url of
252 | Just ( Just accessToken, _ ) ->
253 | parseUrlQuery url Empty (Query.map Success <| authorizationSuccessParser accessToken)
254 |
255 | Just ( _, Just error ) ->
256 | parseUrlQuery url Empty (Query.map Error <| authorizationErrorParser error)
257 |
258 | _ ->
259 | Empty
260 |
261 |
262 | {-| Parsers used in the [`parseToken`](#parseToken) function.
263 |
264 | - `tokenParser`: Looks for an `access_token` and `token_type` to build a `Token`
265 | - `errorParser`: Looks for an `error` to build a corresponding `ErrorCode`
266 | - `authorizationSuccessParser`: Selected when the `tokenParser` succeeded to parse the remaining parts
267 | - `authorizationErrorParser`: Selected when the `errorParser` succeeded to parse the remaining parts
268 |
269 | -}
270 | type alias Parsers error success =
271 | { tokenParser : Query.Parser (Maybe Token)
272 | , errorParser : Query.Parser (Maybe ErrorCode)
273 | , authorizationSuccessParser : Token -> Query.Parser success
274 | , authorizationErrorParser : ErrorCode -> Query.Parser error
275 | }
276 |
277 |
278 | {-| Default parsers according to RFC-6749.
279 | -}
280 | defaultParsers : Parsers AuthorizationError AuthorizationSuccess
281 | defaultParsers =
282 | { tokenParser = defaultTokenParser
283 | , errorParser = defaultErrorParser
284 | , authorizationSuccessParser = defaultAuthorizationSuccessParser
285 | , authorizationErrorParser = defaultAuthorizationErrorParser
286 | }
287 |
288 |
289 | {-| Default `access_token` parser according to RFC-6749.
290 | -}
291 | defaultTokenParser : Query.Parser (Maybe Token)
292 | defaultTokenParser =
293 | tokenParser
294 |
295 |
296 | {-| Default `error` parser according to RFC-6749.
297 | -}
298 | defaultErrorParser : Query.Parser (Maybe ErrorCode)
299 | defaultErrorParser =
300 | errorParser errorCodeFromString
301 |
302 |
303 | {-| Default response success parser according to RFC-6749.
304 | -}
305 | defaultAuthorizationSuccessParser : Token -> Query.Parser AuthorizationSuccess
306 | defaultAuthorizationSuccessParser accessToken =
307 | Query.map3 (AuthorizationSuccess accessToken Nothing)
308 | expiresInParser
309 | scopeParser
310 | stateParser
311 |
312 |
313 | {-| Default response error parser according to RFC-6749.
314 | -}
315 | defaultAuthorizationErrorParser : ErrorCode -> Query.Parser AuthorizationError
316 | defaultAuthorizationErrorParser =
317 | authorizationErrorParser
318 |
--------------------------------------------------------------------------------
/src/OAuth/Password.elm:
--------------------------------------------------------------------------------
1 | module OAuth.Password exposing
2 | ( makeTokenRequest, Authentication, Credentials, AuthenticationSuccess, AuthenticationError, RequestParts
3 | , defaultAuthenticationSuccessDecoder, defaultAuthenticationErrorDecoder
4 | , makeTokenRequestWith, defaultExpiresInDecoder, defaultScopeDecoder, lenientScopeDecoder, defaultTokenDecoder, defaultRefreshTokenDecoder, defaultErrorDecoder, defaultErrorDescriptionDecoder, defaultErrorUriDecoder
5 | )
6 |
7 | {-| The resource owner password credentials grant type is suitable in
8 | cases where the resource owner has a trust relationship with the
9 | client, such as the device operating system or a highly privileged
10 | application. The authorization server should take special care when
11 | enabling this grant type and only allow it when other flows are not
12 | viable.
13 |
14 | There's only one step in this process:
15 |
16 | - The client authenticates itself directly using the resource owner (user) credentials
17 |
18 | After this step, the client owns a `Token` that can be used to authorize any subsequent
19 | request.
20 |
21 |
22 | ## Authenticate
23 |
24 | @docs makeTokenRequest, Authentication, Credentials, AuthenticationSuccess, AuthenticationError, RequestParts
25 |
26 |
27 | ## JSON Decoders
28 |
29 | @docs defaultAuthenticationSuccessDecoder, defaultAuthenticationErrorDecoder
30 |
31 |
32 | ## Custom Decoders & Parsers (advanced)
33 |
34 | @docs makeTokenRequestWith, defaultExpiresInDecoder, defaultScopeDecoder, lenientScopeDecoder, defaultTokenDecoder, defaultRefreshTokenDecoder, defaultErrorDecoder, defaultErrorDescriptionDecoder, defaultErrorUriDecoder
35 |
36 | -}
37 |
38 | import Dict as Dict exposing (Dict)
39 | import Http
40 | import Internal as Internal exposing (..)
41 | import Json.Decode as Json
42 | import OAuth exposing (ErrorCode(..), GrantType(..), Token, errorCodeFromString, grantTypeToString)
43 | import Url exposing (Url)
44 | import Url.Builder as Builder
45 |
46 |
47 | {-| Request configuration for a Password authentication
48 |
49 | - `credentials` (_RECOMMENDED_):
50 | Credentials needed for `Basic` authentication, if needed by the
51 | authorization server.
52 |
53 | - `url` (_REQUIRED_):
54 | The token endpoint to contact the authorization server.
55 |
56 | - `scope` (_OPTIONAL_):
57 | The scope of the access request.
58 |
59 | - `password` (_REQUIRED_):
60 | Resource owner's password
61 |
62 | - `username` (_REQUIRED_):
63 | Resource owner's username
64 |
65 | -}
66 | type alias Authentication =
67 | { credentials : Maybe Credentials
68 | , url : Url
69 | , scope : List String
70 | , username : String
71 | , password : String
72 | }
73 |
74 |
75 | {-| Describes at least a `clientId` and if defined, a complete set of credentials
76 | with the `secret`. Optional but may be required by the authorization server you
77 | interact with to perform a 'Basic' authentication on top of the authentication request.
78 |
79 | { clientId = ""
80 | , secret = ""
81 | }
82 |
83 | -}
84 | type alias Credentials =
85 | { clientId : String, secret : String }
86 |
87 |
88 | {-| The response obtained as a result of an authentication:
89 |
90 | - `token` (_REQUIRED_):
91 | The access token issued by the authorization server.
92 |
93 | - `refreshToken` (_OPTIONAL_):
94 | The refresh token, which can be used to obtain new access tokens using the same authorization
95 | grant as described in [Section 6](https://tools.ietf.org/html/rfc6749#section-6).
96 |
97 | - `expiresIn` (_RECOMMENDED_):
98 | The lifetime in seconds of the access token. For example, the value "3600" denotes that the
99 | access token will expire in one hour from the time the response was generated. If omitted, the
100 | authorization server SHOULD provide the expiration time via other means or document the default
101 | value.
102 |
103 | - `scope` (_OPTIONAL, if identical to the scope requested; otherwise, REQUIRED_):
104 | The scope of the access token as described by [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3).
105 |
106 | -}
107 | type alias AuthenticationSuccess =
108 | { token : Token
109 | , refreshToken : Maybe Token
110 | , expiresIn : Maybe Int
111 | , scope : List String
112 | }
113 |
114 |
115 | {-| Describes an OAuth error as a result of a request failure
116 |
117 | - `error` (_REQUIRED_):
118 | A single ASCII error code.
119 |
120 | - `errorDescription` (_OPTIONAL_)
121 | Human-readable ASCII text providing additional information, used to assist the client developer in
122 | understanding the error that occurred. Values for the `errorDescription` parameter MUST NOT
123 | include characters outside the set `%x20-21 / %x23-5B / %x5D-7E`.
124 |
125 | - `errorUri` (_OPTIONAL_):
126 | A URI identifying a human-readable web page with information about the error, used to
127 | provide the client developer with additional information about the error. Values for the
128 | `errorUri` parameter MUST conform to the URI-reference syntax and thus MUST NOT include
129 | characters outside the set `%x21 / %x23-5B / %x5D-7E`.
130 |
131 | -}
132 | type alias AuthenticationError =
133 | { error : ErrorCode
134 | , errorDescription : Maybe String
135 | , errorUri : Maybe String
136 | }
137 |
138 |
139 | {-| Parts required to build a request. This record is given to `Http.request` in order
140 | to create a new request and may be adjusted at will.
141 | -}
142 | type alias RequestParts a =
143 | { method : String
144 | , headers : List Http.Header
145 | , url : String
146 | , body : Http.Body
147 | , expect : Http.Expect a
148 | , timeout : Maybe Float
149 | , tracker : Maybe String
150 | }
151 |
152 |
153 | {-| Builds the request components required to get a token in exchange of the resource owner (user) credentials
154 |
155 | let req : Http.Request TokenResponse
156 | req = makeTokenRequest toMsg authentication |> Http.request
157 |
158 | -}
159 | makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg
160 | makeTokenRequest =
161 | makeTokenRequestWith Password defaultAuthenticationSuccessDecoder Dict.empty
162 |
163 |
164 |
165 | --
166 | -- Json Decoders
167 | --
168 |
169 |
170 | {-| Json decoder for a positive response. You may provide a custom response decoder using other decoders
171 | from this module, or some of your own craft.
172 |
173 | defaultAuthenticationSuccessDecoder : Decoder AuthenticationSuccess
174 | defaultAuthenticationSuccessDecoder =
175 | D.map4 AuthenticationSuccess
176 | tokenDecoder
177 | refreshTokenDecoder
178 | expiresInDecoder
179 | scopeDecoder
180 |
181 | -}
182 | defaultAuthenticationSuccessDecoder : Json.Decoder AuthenticationSuccess
183 | defaultAuthenticationSuccessDecoder =
184 | Internal.authenticationSuccessDecoder
185 |
186 |
187 | {-| Json decoder for an errored response.
188 |
189 | case res of
190 | Err (Http.BadStatus { body }) ->
191 | case Json.decodeString OAuth.Password.defaultAuthenticationErrorDecoder body of
192 | Ok { error, errorDescription } ->
193 | doSomething
194 |
195 | _ ->
196 | parserFailed
197 |
198 | _ ->
199 | someOtherError
200 |
201 | -}
202 | defaultAuthenticationErrorDecoder : Json.Decoder AuthenticationError
203 | defaultAuthenticationErrorDecoder =
204 | Internal.authenticationErrorDecoder defaultErrorDecoder
205 |
206 |
207 |
208 | --
209 | -- Custom Decoders & Parsers (advanced)
210 | --
211 |
212 |
213 | {-| Like [`makeTokenRequest`](#makeTokenRequest), but gives you the ability to specify custom grant
214 | type and extra fields to be set on the query.
215 |
216 | makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg
217 | makeTokenRequest =
218 | makeTokenRequestWith Password defaultAuthenticationSuccessDecoder Dict.empty
219 |
220 | -}
221 | makeTokenRequestWith : GrantType -> Json.Decoder success -> Dict String String -> (Result Http.Error success -> msg) -> Authentication -> RequestParts msg
222 | makeTokenRequestWith grantType decoder extraFields toMsg { credentials, password, scope, url, username } =
223 | let
224 | body =
225 | [ Builder.string "grant_type" (grantTypeToString grantType)
226 | , Builder.string "username" username
227 | , Builder.string "password" password
228 | ]
229 | |> urlAddExtraFields extraFields
230 | |> urlAddList "scope" scope
231 | |> Builder.toQuery
232 | |> String.dropLeft 1
233 |
234 | headers =
235 | makeHeaders credentials
236 | in
237 | makeRequest decoder toMsg url headers body
238 |
239 |
240 | {-| Json decoder for the `expiresIn` field.
241 | -}
242 | defaultExpiresInDecoder : Json.Decoder (Maybe Int)
243 | defaultExpiresInDecoder =
244 | Internal.expiresInDecoder
245 |
246 |
247 | {-| Json decoder for the `scope` field (space-separated).
248 | -}
249 | defaultScopeDecoder : Json.Decoder (List String)
250 | defaultScopeDecoder =
251 | Internal.scopeDecoder
252 |
253 |
254 | {-| Json decoder for the `scope` field (comma- or space-separated).
255 | -}
256 | lenientScopeDecoder : Json.Decoder (List String)
257 | lenientScopeDecoder =
258 | Internal.lenientScopeDecoder
259 |
260 |
261 | {-| Json decoder for the `access_token` field.
262 | -}
263 | defaultTokenDecoder : Json.Decoder Token
264 | defaultTokenDecoder =
265 | Internal.tokenDecoder
266 |
267 |
268 | {-| Json decoder for the `refresh_token` field.
269 | -}
270 | defaultRefreshTokenDecoder : Json.Decoder (Maybe Token)
271 | defaultRefreshTokenDecoder =
272 | Internal.refreshTokenDecoder
273 |
274 |
275 | {-| Json decoder for the `error` field.
276 | -}
277 | defaultErrorDecoder : Json.Decoder ErrorCode
278 | defaultErrorDecoder =
279 | Internal.errorDecoder errorCodeFromString
280 |
281 |
282 | {-| Json decoder for the `error_description` field.
283 | -}
284 | defaultErrorDescriptionDecoder : Json.Decoder (Maybe String)
285 | defaultErrorDescriptionDecoder =
286 | Internal.errorDescriptionDecoder
287 |
288 |
289 | {-| Json decoder for the `error_uri` field.
290 | -}
291 | defaultErrorUriDecoder : Json.Decoder (Maybe String)
292 | defaultErrorUriDecoder =
293 | Internal.errorUriDecoder
294 |
--------------------------------------------------------------------------------
/src/OAuth/Refresh.elm:
--------------------------------------------------------------------------------
1 | module OAuth.Refresh exposing
2 | ( makeTokenRequest, Authentication, Credentials, AuthenticationSuccess, AuthenticationError, RequestParts
3 | , defaultAuthenticationSuccessDecoder, defaultAuthenticationErrorDecoder
4 | , makeTokenRequestWith, defaultExpiresInDecoder, defaultScopeDecoder, lenientScopeDecoder, defaultTokenDecoder, defaultRefreshTokenDecoder, defaultErrorDecoder, defaultErrorDescriptionDecoder, defaultErrorUriDecoder
5 | )
6 |
7 | {-| If the authorization server issued a refresh token to the client, the
8 | client may make a refresh request to the token endpoint to obtain a new access token
9 | (and refresh token) from the authorization server.
10 |
11 | There's only one step in this process:
12 |
13 | - The client authenticates itself directly using the previously obtained refresh token
14 |
15 | After this step, the client owns a fresh access `Token` and possibly, a new refresh `Token`. Both
16 | can be used in subsequent requests.
17 |
18 |
19 | ## Authenticate
20 |
21 | @docs makeTokenRequest, Authentication, Credentials, AuthenticationSuccess, AuthenticationError, RequestParts
22 |
23 |
24 | ## JSON Decoders
25 |
26 | @docs defaultAuthenticationSuccessDecoder, defaultAuthenticationErrorDecoder
27 |
28 |
29 | ## Custom Decoders & Parsers (advanced)
30 |
31 | @docs makeTokenRequestWith, defaultExpiresInDecoder, defaultScopeDecoder, lenientScopeDecoder, defaultTokenDecoder, defaultRefreshTokenDecoder, defaultErrorDecoder, defaultErrorDescriptionDecoder, defaultErrorUriDecoder
32 |
33 | -}
34 |
35 | import Dict as Dict exposing (Dict)
36 | import Http
37 | import Internal as Internal exposing (..)
38 | import Json.Decode as Json
39 | import OAuth exposing (ErrorCode(..), GrantType(..), Token, errorCodeFromString, grantTypeToString)
40 | import Url exposing (Url)
41 | import Url.Builder as Builder
42 |
43 |
44 | {-| Request configuration for a Refresh authentication
45 |
46 | - `credentials` (_RECOMMENDED_):
47 | Credentials needed for Basic authentication, if needed by the
48 | authorization server.
49 |
50 | - `url` (_REQUIRED_):
51 | The token endpoint to contact the authorization server.
52 |
53 | - `scope` (_OPTIONAL_):
54 | The scope of the access request.
55 |
56 | - `token` (_REQUIRED_):
57 | Token endpoint of the resource provider
58 |
59 | -}
60 | type alias Authentication =
61 | { credentials : Maybe Credentials
62 | , url : Url
63 | , scope : List String
64 | , token : Token
65 | }
66 |
67 |
68 | {-| Describes a couple of client credentials used for Basic authentication
69 |
70 | { clientId = ""
71 | , secret = ""
72 | }
73 |
74 | -}
75 | type alias Credentials =
76 | { clientId : String, secret : String }
77 |
78 |
79 | {-| The response obtained as a result of an authentication (implicit or not)
80 |
81 | - `token` (_REQUIRED_):
82 | The access token issued by the authorization server.
83 |
84 | - `refreshToken` (_OPTIONAL_):
85 | The refresh token, which can be used to obtain new access tokens using the same authorization
86 | grant as described in [Section 6](https://tools.ietf.org/html/rfc6749#section-6).
87 |
88 | - `expiresIn` (_RECOMMENDED_):
89 | The lifetime in seconds of the access token. For example, the value "3600" denotes that the
90 | access token will expire in one hour from the time the response was generated. If omitted, the
91 | authorization server SHOULD provide the expiration time via other means or document the default
92 | value.
93 |
94 | - `scope` (_OPTIONAL, if identical to the scope requested; otherwise, REQUIRED_):
95 | The scope of the access token as described by [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3).
96 |
97 | -}
98 | type alias AuthenticationSuccess =
99 | { token : Token
100 | , refreshToken : Maybe Token
101 | , expiresIn : Maybe Int
102 | , scope : List String
103 | }
104 |
105 |
106 | {-| Describes an OAuth error as a result of a request failure
107 |
108 | - `error` (_REQUIRED_):
109 | A single ASCII error code.
110 |
111 | - `errorDescription` (_OPTIONAL_)
112 | Human-readable ASCII text providing additional information, used to assist the client developer in
113 | understanding the error that occurred. Values for the `errorDescription` parameter MUST NOT
114 | include characters outside the set `%x20-21 / %x23-5B / %x5D-7E`.
115 |
116 | - `errorUri` (_OPTIONAL_):
117 | A URI identifying a human-readable web page with information about the error, used to
118 | provide the client developer with additional information about the error. Values for the
119 | `errorUri` parameter MUST conform to the URI-reference syntax and thus MUST NOT include
120 | characters outside the set `%x21 / %x23-5B / %x5D-7E`.
121 |
122 | -}
123 | type alias AuthenticationError =
124 | { error : ErrorCode
125 | , errorDescription : Maybe String
126 | , errorUri : Maybe String
127 | }
128 |
129 |
130 | {-| Parts required to build a request. This record is given to [`Http.request`](https://package.elm-lang.org/packages/elm/http/latest/Http#request)
131 | in order to create a new request and may be adjusted at will.
132 | -}
133 | type alias RequestParts a =
134 | { method : String
135 | , headers : List Http.Header
136 | , url : String
137 | , body : Http.Body
138 | , expect : Http.Expect a
139 | , timeout : Maybe Float
140 | , tracker : Maybe String
141 | }
142 |
143 |
144 | {-| Builds the request components required to refresh a token
145 |
146 | let req : Http.Request TokenResponse
147 | req = makeTokenRequest toMsg reqParts |> Http.request
148 |
149 | -}
150 | makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg
151 | makeTokenRequest =
152 | makeTokenRequestWith RefreshToken defaultAuthenticationSuccessDecoder Dict.empty
153 |
154 |
155 |
156 | --
157 | -- Json Decoders
158 | --
159 |
160 |
161 | {-| Json decoder for a positive response. You may provide a custom response decoder using other decoders
162 | from this module, or some of your own craft.
163 |
164 | defaultAuthenticationSuccessDecoder : Decoder AuthenticationSuccess
165 | defaultAuthenticationSuccessDecoder =
166 | D.map4 AuthenticationSuccess
167 | tokenDecoder
168 | refreshTokenDecoder
169 | expiresInDecoder
170 | scopeDecoder
171 |
172 | -}
173 | defaultAuthenticationSuccessDecoder : Json.Decoder AuthenticationSuccess
174 | defaultAuthenticationSuccessDecoder =
175 | Internal.authenticationSuccessDecoder
176 |
177 |
178 | {-| Json decoder for an errored response.
179 |
180 | case res of
181 | Err (Http.BadStatus { body }) ->
182 | case Json.decodeString OAuth.ClientCredentials.defaultAuthenticationErrorDecoder body of
183 | Ok { error, errorDescription } ->
184 | doSomething
185 |
186 | _ ->
187 | parserFailed
188 |
189 | _ ->
190 | someOtherError
191 |
192 | -}
193 | defaultAuthenticationErrorDecoder : Json.Decoder AuthenticationError
194 | defaultAuthenticationErrorDecoder =
195 | Internal.authenticationErrorDecoder defaultErrorDecoder
196 |
197 |
198 |
199 | --
200 | -- Custom Decoders & Parsers (advanced)
201 | --
202 |
203 |
204 | {-| Like [`makeTokenRequest`](#makeTokenRequest), but gives you the ability to specify custom grant
205 | type and extra fields to be set on the query.
206 |
207 | makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg
208 | makeTokenRequest =
209 | makeTokenRequestWith RefreshToken defaultAuthenticationSuccessDecoder Dict.empty
210 |
211 | -}
212 | makeTokenRequestWith : GrantType -> Json.Decoder success -> Dict String String -> (Result Http.Error success -> msg) -> Authentication -> RequestParts msg
213 | makeTokenRequestWith grantType decoder extraFields toMsg { credentials, scope, token, url } =
214 | let
215 | body =
216 | [ Builder.string "grant_type" (grantTypeToString grantType)
217 | , Builder.string "refresh_token" (extractTokenString token)
218 | ]
219 | |> urlAddList "scope" scope
220 | |> urlAddExtraFields extraFields
221 | |> Builder.toQuery
222 | |> String.dropLeft 1
223 |
224 | headers =
225 | makeHeaders credentials
226 | in
227 | makeRequest decoder toMsg url headers body
228 |
229 |
230 | {-| Json decoder for the `expiresIn` field.
231 | -}
232 | defaultExpiresInDecoder : Json.Decoder (Maybe Int)
233 | defaultExpiresInDecoder =
234 | Internal.expiresInDecoder
235 |
236 |
237 | {-| Json decoder for the `scope` field (space-separated).
238 | -}
239 | defaultScopeDecoder : Json.Decoder (List String)
240 | defaultScopeDecoder =
241 | Internal.scopeDecoder
242 |
243 |
244 | {-| Json decoder for the `scope` field (comma- or space-separated).
245 | -}
246 | lenientScopeDecoder : Json.Decoder (List String)
247 | lenientScopeDecoder =
248 | Internal.lenientScopeDecoder
249 |
250 |
251 | {-| Json decoder for the `access_token` field.
252 | -}
253 | defaultTokenDecoder : Json.Decoder Token
254 | defaultTokenDecoder =
255 | Internal.tokenDecoder
256 |
257 |
258 | {-| Json decoder for the `refresh_token` field.
259 | -}
260 | defaultRefreshTokenDecoder : Json.Decoder (Maybe Token)
261 | defaultRefreshTokenDecoder =
262 | Internal.refreshTokenDecoder
263 |
264 |
265 | {-| Json decoder for the `error` field
266 | -}
267 | defaultErrorDecoder : Json.Decoder ErrorCode
268 | defaultErrorDecoder =
269 | Internal.errorDecoder errorCodeFromString
270 |
271 |
272 | {-| Json decoder for the `error_description` field
273 | -}
274 | defaultErrorDescriptionDecoder : Json.Decoder (Maybe String)
275 | defaultErrorDescriptionDecoder =
276 | Internal.errorDescriptionDecoder
277 |
278 |
279 | {-| Json decoder for the `error_uri` field
280 | -}
281 | defaultErrorUriDecoder : Json.Decoder (Maybe String)
282 | defaultErrorUriDecoder =
283 | Internal.errorUriDecoder
284 |
--------------------------------------------------------------------------------
/tests/Test/Parsers.elm:
--------------------------------------------------------------------------------
1 | module Test.Parsers exposing (..)
2 |
3 | import Expect exposing (Expectation)
4 | import Fuzz exposing (Fuzzer, int, list, string)
5 | import OAuth exposing (tokenFromString)
6 | import OAuth.Implicit as Implicit
7 | import Test exposing (..)
8 | import Url exposing (Protocol(..), Url)
9 | import Url.Parser.Query as Query
10 |
11 |
12 | suite : Test
13 | suite =
14 | describe "parseTokenWith"
15 | [ test "example #1" <|
16 | \_ ->
17 | let
18 | url =
19 | { fragment =
20 | Just <|
21 | String.join "&"
22 | [ "access_token=eyJ0ePPjuBg"
23 | , "token_type=bearer"
24 | , "expires_in=3600"
25 | , "state=z31j7AMHBiAHySvY8PvtcA=="
26 | ]
27 | , host = "localhost"
28 | , path = "/dashboard"
29 | , port_ = Just 4200
30 | , protocol = Http
31 | , query = Nothing
32 | }
33 |
34 | result =
35 | Implicit.parseTokenWith Implicit.defaultParsers url
36 | in
37 | case result of
38 | Implicit.Success authorization ->
39 | Expect.all
40 | [ \{ token } ->
41 | Just token
42 | |> Expect.equal (tokenFromString "Bearer=eyJ0ePPjuBg")
43 | , \{ refreshToken } ->
44 | refreshToken |> Expect.equal Nothing
45 | , \{ expiresIn } ->
46 | expiresIn
47 | |> Expect.equal (Just 3600)
48 | , \{ scope } ->
49 | scope
50 | |> Expect.equal []
51 | , \{ state } ->
52 | state
53 | |> Expect.equal (Just "z31j7AMHBiAHySvY8PvtcA==")
54 | ]
55 | authorization
56 |
57 | _ ->
58 | Expect.fail "Expected parser to succeed"
59 | ]
60 |
--------------------------------------------------------------------------------