├── .gitignore ├── src ├── styles.css ├── index.html └── Main.elm ├── elm-package.json ├── package.json ├── LICENSE ├── gulpfile.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff 2 | /node_modules 3 | /dist 4 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 1em auto; 3 | max-width: 600px; 4 | } 5 | blockquote { 6 | margin: 1em 0; 7 | } 8 | .jumbotron { 9 | margin: 2em auto; 10 | max-width: 400px; 11 | } 12 | .jumbotron h2 { 13 | margin-top: 0; 14 | } 15 | .jumbotron .help-block { 16 | font-size: 14px; 17 | } -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Build an App in Elm with JWT Authentication and an API", 4 | "repository": "https://github.com/auth0-blog/elm-with-jwt-api.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | "src", 8 | "dist" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 13 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 14 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 15 | "rgrempel/elm-http-decorators": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-app-jwt-api", 3 | "version": "1.0.0", 4 | "author": "Auth0", 5 | "description": "Authenticating an Elm App with JWT", 6 | "main": "", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/auth0-blog/elm-app-jwt-api.git" 10 | }, 11 | "keywords": [ 12 | "elm" 13 | ], 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/auth0-blog/elm-app-jwt-api/issues" 17 | }, 18 | "scripts": { 19 | "dev": "gulp", 20 | "build": "gulp build" 21 | }, 22 | "homepage": "https://github.com/auth0-blog/elm-app-jwt-api", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "gulp": "^3.9.0", 26 | "gulp-connect": "^4.0.0", 27 | "gulp-elm": "^0.6.1", 28 | "gulp-plumber": "^1.1.0", 29 | "gulp-util": "^3.0.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chuck Norris Quoter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Auth0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var elm = require('gulp-elm'); 3 | var gutil = require('gulp-util'); 4 | var plumber = require('gulp-plumber'); 5 | var connect = require('gulp-connect'); 6 | 7 | // File paths 8 | var paths = { 9 | dest: 'dist', 10 | elm: 'src/*.elm', 11 | static: 'src/*.{html,css}' 12 | }; 13 | 14 | // Init Elm 15 | gulp.task('elm-init', elm.init); 16 | 17 | // Compile Elm 18 | gulp.task('elm', ['elm-init'], function(){ 19 | return gulp.src(paths.elm) 20 | .pipe(plumber()) 21 | .pipe(elm()) 22 | .pipe(gulp.dest(paths.dest)); 23 | }); 24 | 25 | // Move static assets to dist 26 | gulp.task('static', function() { 27 | return gulp.src(paths.static) 28 | .pipe(plumber()) 29 | .pipe(gulp.dest(paths.dest)); 30 | }); 31 | 32 | // Watch for changes and compile 33 | gulp.task('watch', function() { 34 | gulp.watch(paths.elm, ['elm']); 35 | gulp.watch(paths.static, ['static']); 36 | }); 37 | 38 | // Local server 39 | gulp.task('connect', function() { 40 | connect.server({ 41 | root: 'dist', 42 | port: 3000 43 | }); 44 | }); 45 | 46 | // Main gulp tasks 47 | gulp.task('build', ['elm', 'static']); 48 | gulp.task('default', ['connect', 'build', 'watch']); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creating Your First Elm App: From Authentication to Calling an API 2 | 3 | This is the source code repository for: 4 | 5 | [Creating Your First Elm App: From Authentication to Calling an API (Part 1)](https://auth0.com/blog/creating-your-first-elm-app-part-1) 6 | 7 | [Creating Your First Elm App: From Authentication to Calling an API (Part 2)](https://auth0.com/blog/creating-your-first-elm-app-part-2) 8 | 9 | Tutorial steps for `Main.elm` are available as [branches](https://github.com/auth0-blog/elm-with-jwt-api/branches). 10 | 11 | ## What is Auth0? 12 | 13 | Auth0 helps you to: 14 | 15 | * Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. 16 | * Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**. 17 | * Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user. 18 | * Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely. 19 | * Analytics of how, when and where users are logging in. 20 | * Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules). 21 | 22 | ## Create a Free Auth0 Account 23 | 24 | 1. Go to [Auth0](https://auth0.com) and click Sign Up. 25 | 2. Use Google, GitHub, or Microsoft Account to log in. 26 | 27 | ## Issue Reporting 28 | 29 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 30 | 31 | ## Author 32 | 33 | [Auth0](auth0.com) 34 | 35 | ## License 36 | 37 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 38 | -------------------------------------------------------------------------------- /src/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Events exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Http 7 | import Json.Decode as Decode exposing (..) 8 | import Json.Encode as Encode exposing (..) 9 | 10 | 11 | main : Program (Maybe Model) Model Msg 12 | main = 13 | Html.programWithFlags 14 | { init = init 15 | , update = update 16 | , subscriptions = \_ -> Sub.none 17 | , view = view 18 | } 19 | 20 | 21 | 22 | {- 23 | MODEL 24 | * Model type 25 | * Initialize model with empty values 26 | * Initialize with a random quote 27 | -} 28 | 29 | 30 | type alias Model = 31 | { username : String 32 | , password : String 33 | , token : String 34 | , quote : String 35 | , protectedQuote : String 36 | , errorMsg : String 37 | } 38 | 39 | 40 | init : Maybe Model -> ( Model, Cmd Msg ) 41 | init model = 42 | case model of 43 | Just model -> 44 | ( model, fetchRandomQuoteCmd ) 45 | 46 | Nothing -> 47 | ( Model "" "" "" "" "" "", fetchRandomQuoteCmd ) 48 | 49 | 50 | 51 | {- 52 | UPDATE 53 | * API routes 54 | * GET and POST 55 | * Encode request body 56 | * Decode responses 57 | * Messages 58 | * Ports 59 | * Update case 60 | -} 61 | -- API request URLs 62 | 63 | 64 | api : String 65 | api = 66 | "http://localhost:3001/" 67 | 68 | 69 | randomQuoteUrl : String 70 | randomQuoteUrl = 71 | api ++ "api/random-quote" 72 | 73 | 74 | registerUrl : String 75 | registerUrl = 76 | api ++ "users" 77 | 78 | 79 | loginUrl : String 80 | loginUrl = 81 | api ++ "sessions/create" 82 | 83 | 84 | protectedQuoteUrl : String 85 | protectedQuoteUrl = 86 | api ++ "api/protected/random-quote" 87 | 88 | 89 | 90 | -- GET a random quote (unauthenticated) 91 | 92 | 93 | fetchRandomQuote : Http.Request String 94 | fetchRandomQuote = 95 | Http.getString randomQuoteUrl 96 | 97 | 98 | fetchRandomQuoteCmd : Cmd Msg 99 | fetchRandomQuoteCmd = 100 | Http.send FetchRandomQuoteCompleted fetchRandomQuote 101 | 102 | 103 | fetchRandomQuoteCompleted : Model -> Result Http.Error String -> ( Model, Cmd Msg ) 104 | fetchRandomQuoteCompleted model result = 105 | case result of 106 | Ok newQuote -> 107 | setStorageHelper { model | quote = newQuote } 108 | 109 | Err _ -> 110 | ( model, Cmd.none ) 111 | 112 | 113 | -- Encode user to construct POST request body (for Register and Log In) 114 | 115 | 116 | userEncoder : Model -> Encode.Value 117 | userEncoder model = 118 | Encode.object 119 | [ ( "username", Encode.string model.username ) 120 | , ( "password", Encode.string model.password ) 121 | ] 122 | 123 | 124 | 125 | -- POST register / login request 126 | 127 | 128 | authUser : Model -> String -> Http.Request String 129 | authUser model apiUrl = 130 | let 131 | body = 132 | model 133 | |> userEncoder 134 | |> Http.jsonBody 135 | in 136 | Http.post apiUrl body tokenDecoder 137 | 138 | 139 | authUserCmd : Model -> String -> Cmd Msg 140 | authUserCmd model apiUrl = 141 | Http.send GetTokenCompleted (authUser model apiUrl) 142 | 143 | 144 | 145 | getTokenCompleted : Model -> Result Http.Error String -> ( Model, Cmd Msg ) 146 | getTokenCompleted model result = 147 | case result of 148 | Ok newToken -> 149 | setStorageHelper { model | token = newToken, password = "", errorMsg = "" } 150 | 151 | Err error -> 152 | ( { model | errorMsg = (toString error) }, Cmd.none ) 153 | 154 | 155 | 156 | -- Decode POST response to get access token 157 | 158 | 159 | tokenDecoder : Decoder String 160 | tokenDecoder = 161 | Decode.field "access_token" Decode.string 162 | 163 | 164 | 165 | -- GET request for random protected quote (authenticated) 166 | 167 | 168 | fetchProtectedQuote : Model -> Http.Request String 169 | fetchProtectedQuote model = 170 | { method = "GET" 171 | , headers = [ Http.header "Authorization" ("Bearer " ++ model.token) ] 172 | , url = protectedQuoteUrl 173 | , body = Http.emptyBody 174 | , expect = Http.expectString 175 | , timeout = Nothing 176 | , withCredentials = False 177 | } 178 | |> Http.request 179 | 180 | 181 | fetchProtectedQuoteCmd : Model -> Cmd Msg 182 | fetchProtectedQuoteCmd model = 183 | Http.send FetchProtectedQuoteCompleted (fetchProtectedQuote model) 184 | 185 | 186 | fetchProtectedQuoteCompleted : Model -> Result Http.Error String -> ( Model, Cmd Msg ) 187 | fetchProtectedQuoteCompleted model result = 188 | case result of 189 | Ok newPQuote -> 190 | setStorageHelper { model | protectedQuote = newPQuote } 191 | 192 | Err _ -> 193 | ( model, Cmd.none ) 194 | 195 | 196 | 197 | -- Helper to update model and set localStorage with the updated model 198 | 199 | 200 | setStorageHelper : Model -> ( Model, Cmd Msg ) 201 | setStorageHelper model = 202 | ( model, setStorage model ) 203 | 204 | 205 | 206 | -- Messages 207 | 208 | 209 | type Msg 210 | = GetQuote 211 | | FetchRandomQuoteCompleted (Result Http.Error String) 212 | | SetUsername String 213 | | SetPassword String 214 | | ClickRegisterUser 215 | | ClickLogIn 216 | | GetTokenCompleted (Result Http.Error String) 217 | | GetProtectedQuote 218 | | FetchProtectedQuoteCompleted (Result Http.Error String) 219 | | LogOut 220 | 221 | 222 | 223 | -- Ports 224 | 225 | 226 | port setStorage : Model -> Cmd msg 227 | 228 | 229 | port removeStorage : Model -> Cmd msg 230 | 231 | 232 | 233 | -- Update 234 | 235 | 236 | update : Msg -> Model -> ( Model, Cmd Msg ) 237 | update msg model = 238 | case msg of 239 | GetQuote -> 240 | ( model, fetchRandomQuoteCmd ) 241 | 242 | FetchRandomQuoteCompleted result -> 243 | fetchRandomQuoteCompleted model result 244 | 245 | SetUsername username -> 246 | ( { model | username = username }, Cmd.none ) 247 | 248 | SetPassword password -> 249 | ( { model | password = password }, Cmd.none ) 250 | 251 | ClickRegisterUser -> 252 | ( model, authUserCmd model registerUrl ) 253 | 254 | ClickLogIn -> 255 | ( model, authUserCmd model loginUrl ) 256 | 257 | GetTokenCompleted result -> 258 | getTokenCompleted model result 259 | 260 | GetProtectedQuote -> 261 | ( model, fetchProtectedQuoteCmd model ) 262 | 263 | FetchProtectedQuoteCompleted result -> 264 | fetchProtectedQuoteCompleted model result 265 | 266 | LogOut -> 267 | ( { model | username = "", protectedQuote = "", token = "" }, removeStorage model ) 268 | 269 | 270 | 271 | {- 272 | VIEW 273 | * Hide sections of view depending on authenticaton state of model 274 | * Get a quote 275 | * Log In or Register 276 | * Get a protected quote 277 | -} 278 | 279 | 280 | view : Model -> Html Msg 281 | view model = 282 | let 283 | -- Is the user logged in? 284 | loggedIn : Bool 285 | loggedIn = 286 | if String.length model.token > 0 then 287 | True 288 | else 289 | False 290 | 291 | -- If the user is logged in, show a greeting; if logged out, show the login/register form 292 | authBoxView = 293 | let 294 | -- If there is an error on authentication, show the error alert 295 | showError : String 296 | showError = 297 | if String.isEmpty model.errorMsg then 298 | "hidden" 299 | else 300 | "" 301 | 302 | -- Greet a logged in user by username 303 | greeting : String 304 | greeting = 305 | "Hello, " ++ model.username ++ "!" 306 | in 307 | if loggedIn then 308 | div [ id "greeting" ] 309 | [ h3 [ class "text-center" ] [ text greeting ] 310 | , p [ class "text-center" ] [ text "You have super-secret access to protected quotes." ] 311 | , p [ class "text-center" ] 312 | [ button [ class "btn btn-danger", onClick LogOut ] [ text "Log Out" ] 313 | ] 314 | ] 315 | else 316 | div [ id "form" ] 317 | [ h2 [ class "text-center" ] [ text "Log In or Register" ] 318 | , p [ class "help-block" ] [ text "If you already have an account, please Log In. Otherwise, enter your desired username and password and Register." ] 319 | , div [ class showError ] 320 | [ div [ class "alert alert-danger" ] [ text model.errorMsg ] 321 | ] 322 | , div [ class "form-group row" ] 323 | [ div [ class "col-md-offset-2 col-md-8" ] 324 | [ label [ for "username" ] [ text "Username:" ] 325 | , input [ id "username", type_ "text", class "form-control", Html.Attributes.value model.username, onInput SetUsername ] [] 326 | ] 327 | ] 328 | , div [ class "form-group row" ] 329 | [ div [ class "col-md-offset-2 col-md-8" ] 330 | [ label [ for "password" ] [ text "Password:" ] 331 | , input [ id "password", type_ "password", class "form-control", Html.Attributes.value model.password, onInput SetPassword ] [] 332 | ] 333 | ] 334 | , div [ class "text-center" ] 335 | [ button [ class "btn btn-primary", onClick ClickLogIn ] [ text "Log In" ] 336 | , button [ class "btn btn-link", onClick ClickRegisterUser ] [ text "Register" ] 337 | ] 338 | ] 339 | 340 | -- If user is logged in, show button and quote; if logged out, show a message instructing them to log in 341 | protectedQuoteView = 342 | let 343 | -- If no protected quote, apply a class of "hidden" 344 | hideIfNoProtectedQuote : String 345 | hideIfNoProtectedQuote = 346 | if String.isEmpty model.protectedQuote then 347 | "hidden" 348 | else 349 | "" 350 | in 351 | if loggedIn then 352 | div [] 353 | [ p [ class "text-center" ] 354 | [ button [ class "btn btn-info", onClick GetProtectedQuote ] [ text "Grab a protected quote!" ] 355 | ] 356 | -- Blockquote with protected quote: only show if a protectedQuote is present in model 357 | , blockquote [ class hideIfNoProtectedQuote ] 358 | [ p [] [ text model.protectedQuote ] 359 | ] 360 | ] 361 | else 362 | p [ class "text-center" ] [ text "Please log in or register to see protected quotes." ] 363 | in 364 | div [ class "container" ] 365 | [ h2 [ class "text-center" ] [ text "Chuck Norris Quotes" ] 366 | , p [ class "text-center" ] 367 | [ button [ class "btn btn-success", onClick GetQuote ] [ text "Grab a quote!" ] 368 | ] 369 | -- Blockquote with quote 370 | , blockquote [] 371 | [ p [] [ text model.quote ] 372 | ] 373 | , div [ class "jumbotron text-left" ] 374 | [ -- Login/Register form or user greeting 375 | authBoxView 376 | ] 377 | , div [] 378 | [ h2 [ class "text-center" ] [ text "Protected Chuck Norris Quotes" ] 379 | -- Protected quotes 380 | , protectedQuoteView 381 | ] 382 | ] 383 | --------------------------------------------------------------------------------