├── .gitignore ├── ChangeLog.md ├── README.md ├── backend ├── LICENSE ├── Obelisk │ └── OAuth │ │ └── AccessToken.hs └── obelisk-oauth-backend.cabal ├── cabal.project ├── common ├── LICENSE ├── Obelisk │ └── OAuth │ │ └── Authorization.hs └── obelisk-oauth-common.cabal ├── example ├── .gitignore ├── .obelisk │ └── impl │ │ ├── default.nix │ │ ├── github.json │ │ └── thunk.nix ├── backend │ ├── backend.cabal │ ├── src-bin │ │ └── main.hs │ └── src │ │ └── Backend.hs ├── cabal.project ├── common │ ├── common.cabal │ └── src │ │ └── Common │ │ └── Route.hs ├── config │ ├── common │ │ ├── example │ │ └── route │ └── readme.md ├── default.nix ├── frontend │ ├── frontend.cabal │ ├── src-bin │ │ └── main.hs │ └── src │ │ └── Frontend.hs └── static │ └── .gitignore └── release.nix /.gitignore: -------------------------------------------------------------------------------- 1 | */dist 2 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # `obelisk-oauth` ChangeLog 2 | 3 | `obelisk-oauth` consists of two Haskell libraries: `obelisk-oauth-backend` and `obelisk-oauth-common`. These are released together with matching version numbers. `master` is the release branch. 4 | 5 | ## Version 0.2.0.0 6 | 7 | * [#11](https://github.com/obsidiansystems/obelisk-oauth/issues/11): Take a function that wraps the redirect route (so that it can be further nested) 8 | 9 | ## Version 0.1.0.0 10 | 11 | * Initial release 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # obelisk-oauth 2 | 3 | ## Setup 4 | 5 | This repo contains two packages: `obelisk-oauth-common` and `obelisk-oauth-backend`. 6 | 7 | An example usage of these packages is available in the `example` directory. 8 | 9 | To add these packages to your obelisk project, follow the steps below from your obelisk project root (i.e., the folder you ran `ob init` in). 10 | 11 | ### Add dependency thunk 12 | ```bash 13 | $ mkdir dep 14 | $ cd dep 15 | $ git clone git@github.com:obsidian.systems/obelisk-oauth 16 | $ ob thunk pack obelisk-oauth 17 | ``` 18 | 19 | The last step here (`ob thunk pack`) replaces the cloned repository with a "thunk" that contains all the information obelisk needs to fetch/use the repository when needed. 20 | 21 | Check out `ob thunk --help` to learn more about working with thunks. 22 | 23 | ### Add packages to default.nix 24 | 25 | Your skeleton project's `default.nix` uses the [reflex-platform project infrastructure](https://github.com/reflex-frp/reflex-platform/blob/develop/project/default.nix). We can use the [`packages` field](https://github.com/reflex-frp/reflex-platform/blob/develop/project/default.nix#L53-L58) of the project configuration to add our custom packages, as follows: 26 | 27 | ```nix 28 | project ./. ({ hackGet, ... }: { 29 | packages = { 30 | obelisk-oauth-common = (hackGet ./dep/obelisk-oauth) + "/common"; 31 | obelisk-oauth-backend = (hackGet ./dep/obelisk-oauth) + "/backend"; 32 | ... # other configuration goes here 33 | }; 34 | }) 35 | ``` 36 | 37 | Be sure to add `hackGet` to the list of items to bring into scope. `hackGet` is a nix function defined in reflex-platform that takes a path that points to either a source directory or a packed thunk (in other words, it takes a path to a thunk but doesn't care whether it's packed or unpacked). It produces a path to the source (unpacked if necessary). Once we've got that path, we just need to append the subdirectory paths to the individual repos contained in this repository. 38 | 39 | ### Add packages to cabal files 40 | 41 | Finally, add `obelisk-oauth-common` to the `build-depends` field of `common/common.cabal` and add `obelisk-oauth-common` and `obelisk-oauth-backend` to the `build-depends` field of the library stanza in `backend/backend.cabal`. 42 | 43 | ## `Common.Route` + `Obelisk.OAuth.Authorization` 44 | 45 | Add a sub-route to your backend route and embed the provided OAuth route: 46 | 47 | ```haskell 48 | data BackendRoute :: * -> * where 49 | BackendRoute_Missing :: BackendRoute () 50 | BackendRoute_Api :: BackendRoute () 51 | BackendRoute_OAuth :: BackendRoute (R OAuth) 52 | ``` 53 | 54 | Your backend route encoder should handle this case: 55 | ```haskell 56 | ... 57 | pathComponentEncoder $ \case 58 | BackendRoute_OAuth -> PathSegment "oauth" oauthRouteEncoder 59 | ... 60 | ``` 61 | 62 | ## Frontend 63 | 64 | On the frontend, you need to produce an authorization request link with the appropriate callback embedded. 65 | 66 | For example: 67 | 68 | ```haskell 69 | do 70 | let r = AuthorizationRequest 71 | { _authorizationRequest_responseType = AuthorizationResponseType_Code 72 | , _authorizationRequest_clientId = clientId 73 | , _authorizationRequest_redirectUri = Just BackendRoute_OAuth 74 | , _authorizationRequest_scope = [] 75 | , _authorizationRequest_state = Just "none" 76 | } 77 | grantHref = authorizationRequestHref "https://app.asana.com/-/oauth_authorize" route checkedEncoder r 78 | elAttr "a" ("href" =: grantHref) $ text "Authorize with Asana" 79 | ``` 80 | 81 | ## Backend 82 | 83 | In your backend handler, you'll need to handle the OAuth sub-route you created: 84 | 85 | ```haskell 86 | ... 87 | serve $ \case 88 | BackendRoute_OAuth :/ oauthRoute -> case oauthRoute of 89 | OAuth_RedirectUri :/ redirectParams -> case redirectParams of 90 | Nothing -> liftIO $ error "Expected to receive the authorization code here" 91 | Just (RedirectUriParams code mstate) -> do 92 | let t = TokenRequest 93 | { _tokenRequest_grant = TokenGrant_AuthorizationCode $ T.encodeUtf8 code 94 | , _tokenRequest_clientId = clientId -- Get this from the OAuth authorization server 95 | , _tokenRequest_clientSecret = clientSecret -- Get this from the OAuth authorization server 96 | , _tokenRequest_redirectUri = BackendRoute_OAuth 97 | } 98 | reqUrl = "https://app.asana.com/-/oauth_token" 99 | rsp <- liftIO $ flip httpLbs tlsMgr =<< getOauthToken reqUrl route checkedEncoder t 100 | -- ^ this response should include the access token and probably a refresh token 101 | ... 102 | ``` 103 | -------------------------------------------------------------------------------- /backend/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Obsidian Systems LLC 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Obsidian Systems LLC nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /backend/Obelisk/OAuth/AccessToken.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-| 3 | Description: Implements the access token request workflow described in . 4 | -} 5 | module Obelisk.OAuth.AccessToken where 6 | 7 | import Data.ByteString 8 | import Data.Functor.Identity 9 | import Data.Text (Text) 10 | import qualified Data.Text.Encoding as T 11 | import Network.HTTP.Client (Request(..), parseRequest) 12 | import Network.HTTP.Client.MultipartFormData (partBS, formDataBody) 13 | 14 | import Obelisk.OAuth.Authorization 15 | import Obelisk.Route 16 | 17 | data TokenGrant = TokenGrant_AuthorizationCode ByteString 18 | | TokenGrant_RefreshToken ByteString 19 | 20 | data TokenRequest r = TokenRequest 21 | { _tokenRequest_grant :: TokenGrant 22 | , _tokenRequest_clientId :: Text 23 | , _tokenRequest_clientSecret :: Text 24 | , _tokenRequest_redirectUri :: (R OAuth -> R r) 25 | } 26 | 27 | getOauthToken 28 | :: String -- ^ Request url 29 | -> Text -- ^ Application route 30 | -> Encoder Identity Identity (R (FullRoute r a)) PageName 31 | -> TokenRequest r 32 | -> IO Request 33 | getOauthToken reqUrl appRoute enc t = do 34 | req <- parseRequest reqUrl 35 | let form = 36 | [ partBS "client_id" $ T.encodeUtf8 $ _tokenRequest_clientId t 37 | , partBS "client_secret" $ T.encodeUtf8 $ _tokenRequest_clientSecret t 38 | , partBS "redirect_uri" $ T.encodeUtf8 $ 39 | appRoute <> renderBackendRoute enc (_tokenRequest_redirectUri t $ OAuth_RedirectUri :/ Nothing) 40 | ] ++ case _tokenRequest_grant t of 41 | TokenGrant_AuthorizationCode code -> 42 | [ partBS "grant_type" "authorization_code" 43 | , partBS "code" code 44 | ] 45 | TokenGrant_RefreshToken refresh -> 46 | [ partBS "grant_type" "refresh_token" 47 | , partBS "refresh_token" refresh 48 | ] 49 | formDataBody form $ req { method = "POST" } 50 | -------------------------------------------------------------------------------- /backend/obelisk-oauth-backend.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.0 2 | name: obelisk-oauth-backend 3 | version: 0.2.0.0 4 | synopsis: OAuth helpers for Obelisk backends 5 | description: OAuth helpers for Obelisk backends 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Obsidian Systems LLC 9 | maintainer: maintainer@obsidian.systems 10 | copyright: 2019 Obsidian Systems LLC 11 | category: Web 12 | build-type: Simple 13 | 14 | library 15 | exposed-modules: 16 | Obelisk.OAuth.AccessToken 17 | other-extensions: OverloadedStrings 18 | build-depends: 19 | base >=4.11, 20 | bytestring >=0.10, 21 | http-client, 22 | obelisk-oauth-common, 23 | obelisk-route, 24 | text >=1.2 25 | default-language: Haskell2010 26 | ghc-options: -Wall 27 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | common/ 3 | backend/ 4 | -------------------------------------------------------------------------------- /common/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Obsidian Systems LLC 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Obsidian Systems LLC nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /common/Obelisk/OAuth/Authorization.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE GADTs #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE MultiParamTypeClasses #-} 7 | {-# LANGUAGE OverloadedStrings #-} 8 | {-# LANGUAGE RankNTypes #-} 9 | {-# LANGUAGE ScopedTypeVariables #-} 10 | {-# LANGUAGE StandaloneDeriving #-} 11 | {-# LANGUAGE TemplateHaskell #-} 12 | {-# LANGUAGE TypeApplications #-} 13 | {-# LANGUAGE TypeFamilies #-} 14 | {-# LANGUAGE PolyKinds #-} 15 | {-# LANGUAGE UndecidableInstances #-} 16 | {-| 17 | Description: Implements the authorization grant request workflow described in . 18 | -} 19 | module Obelisk.OAuth.Authorization where 20 | 21 | import Prelude hiding ((.)) 22 | 23 | import Control.Categorical.Bifunctor (first) 24 | import Control.Category ((.)) 25 | import Control.Category.Monoidal (coidl) 26 | import Control.Monad.Error.Class (MonadError) 27 | import Data.Functor.Identity (Identity(..)) 28 | import Data.Map (Map) 29 | import qualified Data.Map as Map 30 | import Data.Text (Text) 31 | import qualified Data.Text as T 32 | import GHC.Generics (Generic) 33 | 34 | import Obelisk.Route 35 | import Obelisk.Route.TH 36 | 37 | -- | The desired response type indicates to the authorization server what type of 38 | -- authorization grant the client application is requesting. 39 | -- 40 | -- The "code" response type is used to request an "authorization code" that can 41 | -- be exchanged for an access token and is appropriate when the client application 42 | -- has a backend (because the token exchange API requires access to the client secret). 43 | -- See section of the specification. 44 | -- 45 | -- The "token" response type is used to request an "implicit grant" of an access token, 46 | -- without authenticating the client application (though the user/resource owner must, 47 | -- of course, still approve). See section 48 | -- of the specification. 49 | -- The implicit grant flow sends the access token is directly to the frontend app as 50 | -- a URI fragment. For security implications, see sections 51 | -- and 52 | -- of the specification. 53 | data AuthorizationResponseType = AuthorizationResponseType_Code -- Authorization grant 54 | | AuthorizationResponseType_Token -- Implicit grant 55 | deriving (Show, Read, Eq, Ord, Generic) 56 | 57 | 58 | -- | Fields of the authorization request, which will ultimately become query string 59 | -- parameters. Described in section of 60 | -- the specification. 61 | data AuthorizationRequest r = AuthorizationRequest 62 | { _authorizationRequest_responseType :: AuthorizationResponseType 63 | -- ^ The type of authorization grant being requested. See 'AuthorizationResponseType'. 64 | , _authorizationRequest_clientId :: Text 65 | -- ^ The client application identifier, issued by the authorization server. See section R r) 67 | -- ^ The client application's callback URI, where it expects to receive the authorization code. See section of the spec. The @r@ represents the client application's route type, of which the OAuth route will be a sub-route. 68 | , _authorizationRequest_scope :: [Text] 69 | -- ^ See section , "Access Token Scope" 70 | , _authorizationRequest_state :: Maybe Text 71 | -- ^ This value will be returned to the client application when the resource server redirects the user to the redirect URI. See section . 72 | } 73 | deriving (Generic) 74 | 75 | -- | Turn an 'AuthorizationRequest' into query string parameters separated by @&@. Key names are 76 | -- defined in of the specification. 77 | -- This does not insert a leading @?@. 78 | authorizationRequestParams 79 | :: Text -- ^ Base url 80 | -> Encoder Identity Identity (R (FullRoute br a)) PageName 81 | -> AuthorizationRequest br 82 | -> Text 83 | authorizationRequestParams route enc ar = encode (queryParametersTextEncoder @Identity @Identity) $ 84 | Map.toList $ fmap Just $ mconcat 85 | [ Map.singleton "response_type" $ case _authorizationRequest_responseType ar of 86 | AuthorizationResponseType_Code -> "code" 87 | AuthorizationResponseType_Token -> "token" 88 | , Map.singleton "client_id" (_authorizationRequest_clientId ar) 89 | , case _authorizationRequest_redirectUri ar of 90 | Nothing -> Map.empty 91 | Just r -> Map.singleton "redirect_uri" $ 92 | route <> renderBackendRoute enc (r $ OAuth_RedirectUri :/ Nothing) 93 | , case _authorizationRequest_scope ar of 94 | [] -> Map.empty 95 | xs -> Map.singleton "scope" $ T.intercalate " " xs 96 | , case _authorizationRequest_state ar of 97 | Nothing -> Map.empty 98 | Just s -> Map.singleton "state" s 99 | ] 100 | 101 | -- | Render the authorization request 102 | authorizationRequestHref 103 | :: Text -- ^ API request url 104 | -> Text -- ^ Base application route url 105 | -> Encoder Identity Identity (R (FullRoute br a)) PageName -- ^ Backend route encoder 106 | -> AuthorizationRequest br 107 | -> Text -- ^ Authorization grant request endpoint with query string 108 | authorizationRequestHref reqUrl appUrl enc ar = 109 | reqUrl <> "?" <> authorizationRequestParams appUrl enc ar 110 | 111 | -- | Parameters that the authorization server is expected to provide when granting 112 | -- an authorization code request. See section 113 | -- of the specification. 114 | data RedirectUriParams = RedirectUriParams 115 | { _redirectUriParams_code :: Text 116 | , _redirectUriParams_state :: Maybe Text 117 | } 118 | deriving (Show, Read, Eq, Ord, Generic) 119 | 120 | -- | An 'Encoder' for 'RedirectUriParams' that conforms to section . 121 | redirectUriParamsEncoder 122 | :: forall parse check. (MonadError Text parse, Applicative check) 123 | => Encoder check parse (Maybe RedirectUriParams) PageName 124 | redirectUriParamsEncoder = first (unitEncoder []) . coidl . redirectUriParamsEncoder' 125 | where 126 | redirectUriParamsEncoder' :: Encoder check parse (Maybe RedirectUriParams) (Map Text (Maybe Text)) 127 | redirectUriParamsEncoder' = unsafeMkEncoder $ EncoderImpl 128 | { _encoderImpl_decode = \m -> case (Map.lookup "code" m, Map.lookup "state" m) of 129 | (Just (Just c), Just s) -> return $ Just $ RedirectUriParams c s 130 | (Just (Just c), Nothing) -> return $ Just $ RedirectUriParams c Nothing 131 | _ -> return Nothing 132 | , _encoderImpl_encode = \case 133 | Just (RedirectUriParams code state) -> Map.fromList $ ("code", Just code) : case state of 134 | Nothing -> [] 135 | Just s -> [("state", Just s)] 136 | Nothing -> Map.empty 137 | } 138 | 139 | -- | The OAuth routes necessary for authorization code grants. This should be made a sub-route 140 | -- of the client application. 141 | data OAuth :: * -> * where 142 | OAuth_RedirectUri :: OAuth (Maybe RedirectUriParams) 143 | 144 | -- | The 'Encoder' of the 'OAuth' route. This should be used by the client app's backend 145 | -- route encoder. 146 | oauthRouteEncoder 147 | :: (MonadError Text check, MonadError Text parse) 148 | => Encoder check parse (R OAuth) PageName 149 | oauthRouteEncoder = pathComponentEncoder $ \case 150 | OAuth_RedirectUri -> PathSegment "redirect" redirectUriParamsEncoder 151 | 152 | deriveRouteComponent ''OAuth 153 | -------------------------------------------------------------------------------- /common/obelisk-oauth-common.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.0 2 | name: obelisk-oauth-common 3 | version: 0.2.0.0 4 | synopsis: OAuth helpers for Obelisk apps 5 | description: OAuth helpers for Obelisk apps 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Obsidian Systems LLC 9 | copyright: 2019 Obsidian Systems LLC 10 | maintainer: maintainer@obsidian.systems 11 | category: Web 12 | build-type: Simple 13 | extra-source-files: CHANGELOG.md 14 | 15 | library 16 | exposed-modules: Obelisk.OAuth.Authorization 17 | other-extensions: 18 | DeriveGeneric, 19 | FlexibleContexts, 20 | FlexibleInstances, 21 | GADTs, 22 | KindSignatures, 23 | LambdaCase, 24 | MultiParamTypeClasses, 25 | OverloadedStrings, 26 | RankNTypes, 27 | ScopedTypeVariables, 28 | StandaloneDeriving, 29 | TemplateHaskell, 30 | TypeApplications, 31 | UndecidableInstances 32 | build-depends: 33 | base >=4, 34 | categories, 35 | containers >=0.5, 36 | mtl >=2, 37 | obelisk-route, 38 | text >= 1.2 39 | default-language: Haskell2010 40 | ghc-options: -Wall 41 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | dist-newstyle 2 | result 3 | result-* 4 | result-android 5 | result-ios 6 | result-exe 7 | .attr-cache 8 | ghcid-output.txt 9 | profile/ 10 | -------------------------------------------------------------------------------- /example/.obelisk/impl/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /example/.obelisk/impl/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "obsidiansystems", 3 | "repo": "obelisk", 4 | "branch": "release/0.8.0.0", 5 | "private": false, 6 | "rev": "d9df151ed175be4f2dff721676e412a88a0596c1", 7 | "sha256": "1wm2q4blqga6appp193idkapnqsan7qnkz29kylqag1y11fk4rrj" 8 | } 9 | -------------------------------------------------------------------------------- /example/.obelisk/impl/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /example/backend/backend.cabal: -------------------------------------------------------------------------------- 1 | name: backend 2 | version: 0.1 3 | cabal-version: >= 1.8 4 | build-type: Simple 5 | 6 | library 7 | hs-source-dirs: src 8 | if impl(ghcjs) 9 | buildable: False 10 | build-depends: base 11 | , common 12 | , containers 13 | , frontend 14 | , http-client 15 | , http-client-tls 16 | , obelisk-backend 17 | , obelisk-executable-config-lookup 18 | , obelisk-oauth-backend 19 | , obelisk-oauth-common 20 | , obelisk-route 21 | , text 22 | exposed-modules: 23 | Backend 24 | ghc-options: -Wall 25 | 26 | executable backend 27 | main-is: main.hs 28 | hs-source-dirs: src-bin 29 | if impl(ghcjs) 30 | buildable: False 31 | build-depends: base 32 | , backend 33 | , common 34 | , frontend 35 | , obelisk-backend 36 | -------------------------------------------------------------------------------- /example/backend/src-bin/main.hs: -------------------------------------------------------------------------------- 1 | import Backend 2 | import Frontend 3 | import Obelisk.Backend 4 | 5 | main :: IO () 6 | main = runBackend backend frontend 7 | -------------------------------------------------------------------------------- /example/backend/src/Backend.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | module Backend where 5 | 6 | import Control.Monad.IO.Class (liftIO) 7 | import Data.Map ((!)) 8 | import qualified Data.Text as T 9 | import qualified Data.Text.Encoding as T 10 | import qualified Network.HTTP.Client as Http 11 | import qualified Network.HTTP.Client.TLS as Https 12 | import Obelisk.Route 13 | import Obelisk.Backend (Backend (..)) 14 | import Obelisk.OAuth.AccessToken (TokenRequest (..), TokenGrant (..), getOauthToken) 15 | import Obelisk.OAuth.Authorization (OAuth (..), RedirectUriParams (..)) 16 | import Obelisk.ExecutableConfig.Lookup (getConfigs) 17 | 18 | import Common.Route (BackendRoute (..), FrontendRoute (..), checkedEncoder, fullRouteEncoder) 19 | 20 | backend :: Backend BackendRoute FrontendRoute 21 | backend = Backend 22 | { _backend_run = \serve -> do 23 | cfg <- getConfigs 24 | let route = T.strip $ T.decodeUtf8 $ cfg ! "common/route" 25 | tlsMgr <- Https.newTlsManager 26 | serve $ \case 27 | BackendRoute_Missing :/ () -> error "404" 28 | BackendRoute_OAuth :/ oauthRoute -> case oauthRoute of 29 | OAuth_RedirectUri :/ redirectParams -> case redirectParams of 30 | Nothing -> liftIO $ error "Expected to receive the authorization code here" 31 | Just (RedirectUriParams code _mstate) -> do 32 | let t = TokenRequest 33 | { _tokenRequest_grant = TokenGrant_AuthorizationCode $ T.encodeUtf8 code 34 | , _tokenRequest_clientId = "fake-client-id" -- Get this from the OAuth authorization server 35 | , _tokenRequest_clientSecret = "fake-client-secret" -- Get this from the OAuth authorization server 36 | , _tokenRequest_redirectUri = (\x -> BackendRoute_OAuth :/ x) 37 | } 38 | reqUrl = "https://app.asana.com/-/oauth_token" 39 | rsp <- liftIO $ flip Http.httpLbs tlsMgr =<< getOauthToken reqUrl route checkedEncoder t 40 | -- ^ this response should include the access token and probably a refresh token 41 | liftIO $ print rsp 42 | , _backend_routeEncoder = fullRouteEncoder 43 | } 44 | -------------------------------------------------------------------------------- /example/cabal.project: -------------------------------------------------------------------------------- 1 | optional-packages: 2 | * 3 | -------------------------------------------------------------------------------- /example/common/common.cabal: -------------------------------------------------------------------------------- 1 | name: common 2 | version: 0.1 3 | cabal-version: >= 1.2 4 | build-type: Simple 5 | 6 | library 7 | hs-source-dirs: src 8 | build-depends: base 9 | , mtl 10 | , obelisk-oauth-common 11 | , obelisk-route 12 | , text 13 | exposed-modules: 14 | Common.Route 15 | ghc-options: -Wall 16 | -------------------------------------------------------------------------------- /example/common/src/Common/Route.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE EmptyCase #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE GADTs #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE MultiParamTypeClasses #-} 7 | {-# LANGUAGE OverloadedStrings #-} 8 | {-# LANGUAGE RankNTypes #-} 9 | {-# LANGUAGE TemplateHaskell #-} 10 | {-# LANGUAGE TypeFamilies #-} 11 | module Common.Route where 12 | 13 | import Data.Text (Text) 14 | import Data.Functor.Identity 15 | import Obelisk.OAuth.Authorization (OAuth, oauthRouteEncoder) 16 | 17 | import Obelisk.Route 18 | import Obelisk.Route.TH (deriveRouteComponent) 19 | 20 | data BackendRoute :: * -> * where 21 | -- | Used to handle unparseable routes. 22 | BackendRoute_Missing :: BackendRoute () 23 | BackendRoute_OAuth :: BackendRoute (R OAuth) 24 | -- You can define any routes that will be handled specially by the backend here. 25 | -- i.e. These do not serve the frontend, but do something different, such as serving static files. 26 | 27 | data FrontendRoute :: * -> * where 28 | FrontendRoute_Main :: FrontendRoute () 29 | -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend. 30 | 31 | fullRouteEncoder 32 | :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName 33 | fullRouteEncoder = mkFullRouteEncoder 34 | (FullRoute_Backend BackendRoute_Missing :/ ()) 35 | (\case 36 | BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty 37 | BackendRoute_OAuth -> PathSegment "oauth" oauthRouteEncoder 38 | ) 39 | (\case 40 | FrontendRoute_Main -> PathEnd $ unitEncoder mempty 41 | ) 42 | 43 | checkedEncoder :: Applicative check => Encoder check Identity (R (FullRoute BackendRoute FrontendRoute)) PageName 44 | checkedEncoder = either (error "checkEncoder failed") id $ checkEncoder fullRouteEncoder 45 | 46 | concat <$> mapM deriveRouteComponent 47 | [ ''BackendRoute 48 | , ''FrontendRoute 49 | ] 50 | -------------------------------------------------------------------------------- /example/config/common/example: -------------------------------------------------------------------------------- 1 | This string comes from config/common/example -------------------------------------------------------------------------------- /example/config/common/route: -------------------------------------------------------------------------------- 1 | http://localhost:8000 -------------------------------------------------------------------------------- /example/config/readme.md: -------------------------------------------------------------------------------- 1 | ### Config 2 | 3 | Obelisk projects should contain a config folder with the following subfolders: common, frontend, and backend. 4 | 5 | Things that should never be transmitted to the frontend belong in backend/ (e.g., email credentials) 6 | 7 | Frontend-only configuration belongs in frontend/. 8 | 9 | Shared configuration files (e.g., the route config) belong in common/ 10 | -------------------------------------------------------------------------------- /example/default.nix: -------------------------------------------------------------------------------- 1 | { obelisk ? import ./.obelisk/impl { 2 | system = builtins.currentSystem; 3 | iosSdkVersion = "10.2"; 4 | # You must accept the Android Software Development Kit License Agreement at 5 | # https://developer.android.com/studio/terms in order to build Android apps. 6 | # Uncomment and set this to `true` to indicate your acceptance: 7 | # config.android_sdk.accept_license = false; 8 | } 9 | }: 10 | with obelisk; 11 | project ./. ({ hackGet, ... }: { 12 | android.applicationId = "systems.obsidian.obelisk.examples.minimal"; 13 | android.displayName = "Obelisk Minimal Example"; 14 | ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal"; 15 | ios.bundleName = "Obelisk Minimal Example"; 16 | 17 | packages = { 18 | obelisk-oauth-common = ../common; 19 | obelisk-oauth-backend = ../backend; 20 | }; 21 | }) 22 | -------------------------------------------------------------------------------- /example/frontend/frontend.cabal: -------------------------------------------------------------------------------- 1 | name: frontend 2 | version: 0.1 3 | cabal-version: >= 1.8 4 | build-type: Simple 5 | 6 | library 7 | hs-source-dirs: src 8 | build-depends: base 9 | , common 10 | , containers 11 | , jsaddle 12 | , obelisk-executable-config-lookup 13 | , obelisk-frontend 14 | , obelisk-generated-static 15 | , obelisk-oauth-common 16 | , obelisk-route 17 | , reflex-dom-core 18 | , text 19 | exposed-modules: 20 | Frontend 21 | ghc-options: -Wall 22 | 23 | executable frontend 24 | main-is: main.hs 25 | hs-source-dirs: src-bin 26 | build-depends: base 27 | , common 28 | , obelisk-frontend 29 | , obelisk-route 30 | , reflex-dom 31 | , obelisk-generated-static 32 | , frontend 33 | --TODO: Make these ghc-options optional 34 | ghc-options: -threaded 35 | if os(darwin) 36 | ghc-options: -dynamic 37 | -------------------------------------------------------------------------------- /example/frontend/src-bin/main.hs: -------------------------------------------------------------------------------- 1 | import Frontend 2 | import Common.Route 3 | import Obelisk.Frontend 4 | import Obelisk.Route.Frontend 5 | import Reflex.Dom 6 | 7 | main :: IO () 8 | main = do 9 | let Right validFullEncoder = checkEncoder fullRouteEncoder 10 | run $ runFrontend validFullEncoder frontend 11 | -------------------------------------------------------------------------------- /example/frontend/src/Frontend.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Frontend where 5 | 6 | import Data.Map ((!)) 7 | import qualified Data.Text as T 8 | import qualified Data.Text.Encoding as T 9 | import Obelisk.Frontend (Frontend (..)) 10 | import Obelisk.Configs (getConfigs) 11 | import Obelisk.Route 12 | import Obelisk.OAuth.Authorization (AuthorizationRequest (..), AuthorizationResponseType (..), authorizationRequestHref) 13 | import Reflex.Dom.Core 14 | 15 | import Common.Route (BackendRoute (..), FrontendRoute (..), checkedEncoder) 16 | 17 | 18 | -- This runs in a monad that can be run on the client or the server. 19 | -- To run code in a pure client or pure server context, use one of the 20 | -- `prerender` functions. 21 | frontend :: Frontend (R FrontendRoute) 22 | frontend = Frontend 23 | { _frontend_head = do 24 | el "title" $ text "Obelisk OAuth Minimal Example" 25 | , _frontend_body = do 26 | cfg <- getConfigs 27 | let route = T.strip $ T.decodeUtf8 $ cfg ! "common/route" 28 | 29 | el "h1" $ text "Welcome to Obelisk OAuth!" 30 | let r = AuthorizationRequest 31 | { _authorizationRequest_responseType = AuthorizationResponseType_Code 32 | , _authorizationRequest_clientId = "fake-id" 33 | , _authorizationRequest_redirectUri = Just $ \x -> BackendRoute_OAuth :/ x 34 | , _authorizationRequest_scope = [] 35 | , _authorizationRequest_state = Just "none" 36 | } 37 | grantHref = authorizationRequestHref "https://app.asana.com/-/oauth_authorize" route checkedEncoder r 38 | elAttr "a" ("href" =: grantHref) $ text "Authorize with Asana" 39 | } 40 | -------------------------------------------------------------------------------- /example/static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obsidiansystems/obelisk-oauth/d6c04107c90f7195d9c2da93bf726147cc8de61a/example/static/.gitignore -------------------------------------------------------------------------------- /release.nix: -------------------------------------------------------------------------------- 1 | let 2 | example = import ./example {}; 3 | in { 4 | ghcShell = example.shells.ghc; 5 | ghcsjsShell = example.shells.ghcjs; 6 | exe = example.exe; 7 | } 8 | --------------------------------------------------------------------------------