├── .dockerignore ├── .gitignore ├── .realize └── realize.yaml ├── .travis.yml ├── Gopkg.lock ├── Gopkg.toml ├── MAINTAINERS ├── README.md ├── main.go └── templates ├── callback.html ├── consent.html ├── home.html └── login.html /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | .vagrant/ 4 | docs/ 5 | dist/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | *.iml 4 | *.exe 5 | .vagrant 6 | *.log 7 | *.stackdump 8 | .DS_Store 9 | vendor/ 10 | .hydra.yml 11 | cover.out 12 | output/ 13 | _book/ 14 | dist/ 15 | coverage.* 16 | /hydra-consent-app-go 17 | -------------------------------------------------------------------------------- /.realize/realize.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | resources: 3 | streams: streams.log 4 | logs: logs.log 5 | errors: errors.log 6 | server: 7 | enable: true 8 | open: false 9 | host: localhost 10 | port: 5001 11 | config: 12 | flimit: 0 13 | polling: false 14 | polling_interval: 200ms 15 | kill_on_error: false 16 | projects: 17 | - name: hydra-consent-app-go 18 | path: . 19 | fmt: true 20 | test: false 21 | generate: false 22 | bin: true 23 | build: false 24 | run: true 25 | params: [] 26 | watcher: 27 | before: [] 28 | after: [] 29 | paths: 30 | - / 31 | ignore_paths: 32 | - vendor 33 | exts: 34 | - .go 35 | preview: false 36 | cli: 37 | streams: true 38 | file: 39 | streams: false 40 | logs: false 41 | errors: false 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: go 4 | go_import_path: github.com/ory/hydra-consent-app-go 5 | 6 | go: 7 | - 1.9 8 | 9 | install: 10 | - curl -sL https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64 -o $GOPATH/bin/dep 11 | - chmod +x $GOPATH/bin/dep 12 | - go get github.com/golang/lint/golint 13 | - dep ensure 14 | - go get -d github.com/ory/hydra 15 | - (cd ../../ory/hydra; dep ensure) 16 | - go install github.com/ory/hydra 17 | - go install . 18 | 19 | script: 20 | - go vet ./... 21 | - golint -set_exit_status $(go list ./...) 22 | - export FORCE_ROOT_CLIENT_CREDENTIALS=demo:demo 23 | - export CONSENT_URL=http://localhost:4445/consent 24 | - export DATABASE_URL=memory 25 | - hydra host --dangerous-force-http & 26 | - while ! echo exit | nc localhost 4444; do sleep 1; done 27 | - hydra-consent-app-go & 28 | - while ! echo exit | nc localhost 3000; do sleep 1; done 29 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/go-resty/resty" 6 | packages = ["."] 7 | revision = "9ac9c42358f7c3c69ac9f8610e8790d7c338e85d" 8 | version = "v1.0" 9 | 10 | [[projects]] 11 | name = "github.com/golang/protobuf" 12 | packages = ["proto"] 13 | revision = "11b8df160996e00fd4b55cbaafb3d84ec6d50fa8" 14 | 15 | [[projects]] 16 | name = "github.com/gorilla/context" 17 | packages = ["."] 18 | revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" 19 | version = "v1.1" 20 | 21 | [[projects]] 22 | name = "github.com/gorilla/mux" 23 | packages = ["."] 24 | revision = "392c28fe23e1c45ddba891b0320b3b5df220beea" 25 | version = "v1.3.0" 26 | 27 | [[projects]] 28 | branch = "master" 29 | name = "github.com/gorilla/securecookie" 30 | packages = ["."] 31 | revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983" 32 | 33 | [[projects]] 34 | name = "github.com/gorilla/sessions" 35 | packages = ["."] 36 | revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896" 37 | version = "v1.1" 38 | 39 | [[projects]] 40 | branch = "master" 41 | name = "github.com/meatballhat/negroni-logrus" 42 | packages = ["."] 43 | revision = "31067281800f66f57548a7a32d9c6c5f963fef83" 44 | 45 | [[projects]] 46 | name = "github.com/ory/common" 47 | packages = ["env"] 48 | revision = "ba06ec2f738cb3a55608657c2e998a1eef675423" 49 | version = "v0.1.0" 50 | 51 | [[projects]] 52 | name = "github.com/ory/hydra" 53 | packages = ["sdk/go/hydra","sdk/go/hydra/swagger"] 54 | revision = "f8dd4a16e921b5cb798e7e1bc15867ab7ddb8863" 55 | version = "v0.10.0-alpha.1" 56 | 57 | [[projects]] 58 | name = "github.com/pkg/errors" 59 | packages = ["."] 60 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 61 | version = "v0.8.0" 62 | 63 | [[projects]] 64 | name = "github.com/sirupsen/logrus" 65 | packages = ["."] 66 | revision = "89742aefa4b206dcf400792f3bd35b542998eb3b" 67 | 68 | [[projects]] 69 | name = "github.com/urfave/negroni" 70 | packages = ["."] 71 | revision = "fde5e16d32adc7ad637e9cd9ad21d4ebc6192535" 72 | version = "v0.2.0" 73 | 74 | [[projects]] 75 | name = "golang.org/x/crypto" 76 | packages = ["ssh/terminal"] 77 | revision = "faadfbdc035307d901e69eea569f5dda451a3ee3" 78 | 79 | [[projects]] 80 | name = "golang.org/x/net" 81 | packages = ["context","context/ctxhttp","idna","publicsuffix"] 82 | revision = "859d1a86bb617c0c20d154590c3c5d3fcb670b07" 83 | 84 | [[projects]] 85 | name = "golang.org/x/oauth2" 86 | packages = [".","clientcredentials","internal"] 87 | revision = "13449ad91cb26cb47661c1b080790392170385fd" 88 | 89 | [[projects]] 90 | name = "golang.org/x/sys" 91 | packages = ["unix","windows"] 92 | revision = "062cd7e4e68206d8bab9b18396626e855c992658" 93 | 94 | [[projects]] 95 | branch = "master" 96 | name = "golang.org/x/text" 97 | packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] 98 | revision = "88f656faf3f37f690df1a32515b479415e1a6769" 99 | 100 | [[projects]] 101 | name = "google.golang.org/appengine" 102 | packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"] 103 | revision = "d9a072cfa7b9736e44311ef77b3e09d804bfa599" 104 | 105 | [solve-meta] 106 | analyzer-name = "dep" 107 | analyzer-version = 1 108 | inputs-digest = "8b3e435f2fb1ab9a6621682b622645e0c6c3c289fec60b428302afc970242c8a" 109 | solver-name = "gps-cdcl" 110 | solver-version = 1 111 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/gorilla/mux" 26 | version = "~1.3.0" 27 | 28 | [[constraint]] 29 | name = "github.com/gorilla/sessions" 30 | version = "~1.1.0" 31 | 32 | [[constraint]] 33 | name = "github.com/meatballhat/negroni-logrus" 34 | 35 | [[constraint]] 36 | name = "github.com/ory/common" 37 | version = "~0.1.0" 38 | 39 | [[constraint]] 40 | name = "github.com/ory/hydra" 41 | version = "~0.10.0-alpha.1" 42 | 43 | [[constraint]] 44 | name = "github.com/pkg/errors" 45 | version = "~0.8.0" 46 | 47 | [[constraint]] 48 | name = "github.com/urfave/negroni" 49 | version = "~0.2.0" 50 | 51 | [[constraint]] 52 | name = "golang.org/x/oauth2" 53 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Aeneas Rekkas (github: arekkas) 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hydra-consent-app-go 2 | 3 | **This repository works only with ORY Hydra versions prior to the 1.0.0 release. For a version that works with 1.0.0 check out [this repository](https://github.com/ory/hydra-login-consent-node).** 4 | 5 | [![Build Status](https://travis-ci.org/ory/hydra-consent-app-go.svg?branch=master)](https://travis-ci.org/ory/hydra-consent-app-go) 6 | 7 | This is a simple consent app for Hydra written in Go. It uses the Hydra SDK. 8 | To run the example, first install Hydra, [dep](https://github.com/golang/dep) 9 | and this project: 10 | 11 | ``` 12 | go get -u -d github.com/ory/hydra-consent-app-go 13 | cd $GOPATH/src/github.com/ory/hydra-consent-app-go 14 | dep ensure 15 | ``` 16 | 17 | Next open a shell and run: 18 | 19 | ```sh 20 | export FORCE_ROOT_CLIENT_CREDENTIALS=demo:demo 21 | export CONSENT_URL=http://localhost:3000/consent 22 | hydra host --dangerous-force-http 23 | ``` 24 | 25 | In another console, run 26 | 27 | ``` 28 | hydra-consent-app-go 29 | ``` 30 | 31 | or alternatively, if you're in the project's directory: 32 | 33 | ``` 34 | go run main.go 35 | ``` 36 | 37 | Then, open the browser: 38 | 39 | ``` 40 | open http://localhost:3000/ 41 | ``` 42 | 43 | Now follow the steps described in the browser. If you encounter an error, 44 | use the browser's back button to get back to the last screen. 45 | 46 | Keep in mind that you will not be able to refresh the callback url, as the 47 | authorize code is valid only once. Also, this application needs to run on 48 | port 4445 in order for the demo to work. Usually the consent endpoint won't 49 | perform the authorize code, but for the sake of the demo we added that too. 50 | 51 | Make sure that you stop the docker-compose demo of the Hydra main repository, 52 | otherwise ports 4445 and 4444 are unassignable. 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/gorilla/sessions" 11 | negronilogrus "github.com/meatballhat/negroni-logrus" 12 | "github.com/ory/common/env" 13 | "github.com/ory/hydra/sdk/go/hydra" 14 | "github.com/ory/hydra/sdk/go/hydra/swagger" 15 | "github.com/pkg/errors" 16 | "github.com/urfave/negroni" 17 | "golang.org/x/oauth2" 18 | ) 19 | 20 | // This store will be used to save user authentication 21 | var store = sessions.NewCookieStore([]byte("something-very-secret-keep-it-safe")) 22 | 23 | // The session is a unique session identifier 24 | const sessionName = "authentication" 25 | 26 | // This is the Hydra SDK 27 | var client hydra.SDK 28 | 29 | // A state for performing the OAuth 2.0 flow. This is usually not part of a consent app, but in order for the demo 30 | // to make sense, it performs the OAuth 2.0 authorize code flow. 31 | var state = "demostatedemostatedemo" 32 | 33 | func main() { 34 | var err error 35 | 36 | // Initialize the hydra SDK. The defaults work if you started hydra as described in the README.md 37 | client, err = hydra.NewSDK(&hydra.Configuration{ 38 | ClientID: env.Getenv("HYDRA_CLIENT_ID", "demo"), 39 | ClientSecret: env.Getenv("HYDRA_CLIENT_SECRET", "demo"), 40 | EndpointURL: env.Getenv("HYDRA_CLUSTER_URL", "http://localhost:4444"), 41 | Scopes: []string{"hydra.consent"}, 42 | }) 43 | if err != nil { 44 | log.Fatalf("Unable to connect to the Hydra SDK because %s", err) 45 | } 46 | 47 | // Set up a router and some routes 48 | r := mux.NewRouter() 49 | r.HandleFunc("/", handleHome) 50 | r.HandleFunc("/consent", handleConsent) 51 | r.HandleFunc("/login", handleLogin) 52 | r.HandleFunc("/callback", handleCallback) 53 | 54 | // Set up a request logger, useful for debugging 55 | n := negroni.New() 56 | n.Use(negronilogrus.NewMiddleware()) 57 | n.UseHandler(r) 58 | 59 | // Start http server 60 | log.Println("Listening on :" + env.Getenv("PORT", "3000")) 61 | http.ListenAndServe(":"+env.Getenv("PORT", "3000"), n) 62 | } 63 | 64 | // handles request at /home - a small page that let's you know what you can do in this app. Usually the first. 65 | // page a user sees. 66 | func handleHome(w http.ResponseWriter, _ *http.Request) { 67 | var config = client.GetOAuth2Config() 68 | config.RedirectURL = "http://localhost:4445/callback" 69 | config.Scopes = []string{"offline", "openid"} 70 | 71 | var authURL = client.GetOAuth2Config().AuthCodeURL(state) + "&nonce=" + state 72 | renderTemplate(w, "home.html", authURL) 73 | } 74 | 75 | // After pressing "click here", the Authorize Code flow is performed and the user is redirected to Hydra. Next, Hydra 76 | // validates the consent request (it's not valid yet) and redirects us to the consent endpoint which we set with `CONSENT_URL=http://localhost:4445/consent`. 77 | func handleConsent(w http.ResponseWriter, r *http.Request) { 78 | // Get the consent requerst id from the query. 79 | consentRequestID := r.URL.Query().Get("consent") 80 | if consentRequestID == "" { 81 | http.Error(w, errors.New("Consent endpoint was called without a consent request id").Error(), http.StatusBadRequest) 82 | return 83 | } 84 | 85 | // Fetch consent information 86 | consentRequest, response, err := client.GetOAuth2ConsentRequest(consentRequestID) 87 | if err != nil { 88 | http.Error(w, errors.Wrap(err, "The consent request endpoint does not respond").Error(), http.StatusBadRequest) 89 | return 90 | } else if response.StatusCode != http.StatusOK { 91 | http.Error(w, errors.Wrapf(err, "Consent request endpoint gave status code %d but expected %d", response.StatusCode, http.StatusOK).Error(), http.StatusBadRequest) 92 | return 93 | } 94 | 95 | // This helper checks if the user is already authenticated. If not, we 96 | // redirect them to the login endpoint. 97 | user := authenticated(r) 98 | if user == "" { 99 | http.Redirect(w, r, "/login?consent="+consentRequestID, http.StatusFound) 100 | return 101 | } 102 | 103 | // Apparently, the user is logged in. Now we check if we received POST 104 | // request, or a GET request. 105 | if r.Method == "POST" { 106 | // Ok, apparently the user gave their consent! 107 | 108 | // Parse the HTTP form - required by Go. 109 | if err := r.ParseForm(); err != nil { 110 | http.Error(w, errors.Wrap(err, "Could not parse form").Error(), http.StatusBadRequest) 111 | return 112 | } 113 | 114 | // Let's check which scopes the user granted. 115 | var grantedScopes = []string{} 116 | for key := range r.PostForm { 117 | // And add each scope to the list of granted scopes. 118 | grantedScopes = append(grantedScopes, key) 119 | } 120 | 121 | // Ok, now we accept the consent request. 122 | response, err := client.AcceptOAuth2ConsentRequest(consentRequestID, swagger.ConsentRequestAcceptance{ 123 | // The subject is a string, usually the user id. 124 | Subject: user, 125 | 126 | // The scopes our user granted. 127 | GrantScopes: grantedScopes, 128 | 129 | // Data that will be available on the token introspection and warden endpoints. 130 | AccessTokenExtra: map[string]interface{}{"foo": "bar"}, 131 | 132 | // If we issue an ID token, we can set extra data for that id token here. 133 | IdTokenExtra: map[string]interface{}{"foo": "baz"}, 134 | }) 135 | if err != nil { 136 | http.Error(w, errors.Wrap(err, "The accept consent request endpoint encountered a network error").Error(), http.StatusInternalServerError) 137 | return 138 | } else if response.StatusCode != http.StatusNoContent { 139 | http.Error(w, errors.Wrapf(err, "Accept consent request endpoint gave status code %d but expected %d", response.StatusCode, http.StatusNoContent).Error(), http.StatusInternalServerError) 140 | return 141 | } 142 | 143 | // Redirect the user back to hydra, and append the consent response! If the user denies request you can 144 | // either handle the error in the authentication endpoint, or redirect the user back to the original application 145 | // with: 146 | // 147 | // response, err := client.RejectOAuth2ConsentRequest(consentRequestId, payload) 148 | http.Redirect(w, r, consentRequest.RedirectUrl, http.StatusFound) 149 | return 150 | } 151 | 152 | // We received a get request, so let's show the html site where the user may give consent. 153 | renderTemplate(w, "consent.html", struct { 154 | *swagger.OAuth2ConsentRequest 155 | ConsentRequestID string 156 | }{OAuth2ConsentRequest: consentRequest, ConsentRequestID: consentRequestID}) 157 | } 158 | 159 | // The user hits this endpoint if not authenticated. In this example, they can sign in with the credentials 160 | // buzz:lightyear 161 | func handleLogin(w http.ResponseWriter, r *http.Request) { 162 | consentRequestID := r.URL.Query().Get("consent") 163 | 164 | // Is it a POST request? 165 | if r.Method == "POST" { 166 | // Parse the form 167 | if err := r.ParseForm(); err != nil { 168 | http.Error(w, errors.Wrap(err, "Could not parse form").Error(), http.StatusBadRequest) 169 | return 170 | } 171 | 172 | // Check the user's credentials 173 | if r.Form.Get("username") != "buzz" || r.Form.Get("password") != "lightyear" { 174 | http.Error(w, "Provided credentials are wrong, try buzz:lightyear", http.StatusBadRequest) 175 | return 176 | } 177 | 178 | // Let's create a session where we store the user id. We can ignore errors from the session store 179 | // as it will always return a session! 180 | session, _ := store.Get(r, sessionName) 181 | session.Values["user"] = "buzz-lightyear" 182 | 183 | // Store the session in the cookie 184 | if err := store.Save(r, w, session); err != nil { 185 | http.Error(w, errors.Wrap(err, "Could not persist cookie").Error(), http.StatusBadRequest) 186 | return 187 | } 188 | 189 | // Redirect the user back to the consent endpoint. In a normal app, you would probably 190 | // add some logic here that is triggered when the user actually performs authentication and is not 191 | // part of the consent flow. 192 | http.Redirect(w, r, "/consent?consent="+consentRequestID, http.StatusFound) 193 | return 194 | } 195 | 196 | // It's a get request, so let's render the template 197 | renderTemplate(w, "login.html", consentRequestID) 198 | } 199 | 200 | // Once the user has given their consent, we will hit this endpoint. Again, 201 | // this is not something that would be included in a traditional consent app, 202 | // but we added it so you can see the data once the consent flow is done. 203 | func handleCallback(w http.ResponseWriter, r *http.Request) { 204 | // in the real world you should check the state query parameter, but this is omitted for brevity reasons. 205 | 206 | // Exchange the access code for an access (and optionally) a refresh token 207 | token, err := client.GetOAuth2Config().Exchange(context.Background(), r.URL.Query().Get("code")) 208 | if err != nil { 209 | http.Error(w, errors.Wrap(err, "Could not exhange token").Error(), http.StatusBadRequest) 210 | return 211 | } 212 | 213 | // Render the output 214 | renderTemplate(w, "callback.html", struct { 215 | *oauth2.Token 216 | IDToken interface{} 217 | }{ 218 | Token: token, 219 | IDToken: token.Extra("id_token"), 220 | }) 221 | } 222 | 223 | // authenticated checks if our cookie store has a user stored and returns the 224 | // user's name, or an empty string if the user is not yet authenticated. 225 | func authenticated(r *http.Request) string { 226 | session, _ := store.Get(r, sessionName) 227 | if u, ok := session.Values["user"]; !ok { 228 | return "" 229 | } else if user, ok := u.(string); !ok { 230 | return "" 231 | } else { 232 | return user 233 | } 234 | } 235 | 236 | // renderTemplate is a convenience helper for rendering templates. 237 | func renderTemplate(w http.ResponseWriter, id string, d interface{}) bool { 238 | if t, err := template.New(id).ParseFiles("./templates/" + id); err != nil { 239 | http.Error(w, errors.Wrap(err, "Could not render template").Error(), http.StatusInternalServerError) 240 | return false 241 | } else if err := t.Execute(w, d); err != nil { 242 | http.Error(w, errors.Wrap(err, "Could not render template").Error(), http.StatusInternalServerError) 243 | return false 244 | } 245 | return true 246 | } 247 | -------------------------------------------------------------------------------- /templates/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Success! 6 | 7 | 8 |

9 | OAuth2 authorize code flow was performed successfully! 10 |

11 |
12 |
AccessToken
13 |
{{.AccessToken}}
14 |
TokenType
15 |
{{.TokenType}}
16 |
RefreshToken
17 |
{{.RefreshToken}}
18 |
Expiry
19 |
{{.Expiry}}
20 |
ID Token
21 |
{{.IDToken}}
22 |
23 |

24 | Do it again 25 |

26 | 27 | -------------------------------------------------------------------------------- /templates/consent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Please give your consent 6 | 7 | 8 |

9 | An application (id: {{.ClientId}}) requested consent to access resources on your behalf. The application wants access to: 10 |

11 |
12 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome! 6 | 7 | 8 |

9 | Click here to perform the exemplary authorize code flow. 10 |

11 | 12 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Please sign in to proceed 6 | 7 | 8 |

9 | Please sign in to proceed. 10 |

11 | 12 |
13 | 14 | 15 | 16 |
17 | 18 |

19 | To sign in, use the credentials "buzz:lightyear" 20 |

21 | 22 | 23 | --------------------------------------------------------------------------------