                 }
 
                )))
-         wrap-params wrap-cookies)
+         wrap-params wrap-cookies s/with-fn-validation)
 
      ::exchange-code-for-access-token
      (-> (fn [req]
            (let [params (:form-params req)
                  code (get params "code")
-                 client-id (get params "client_id")]
-             (if-let [{identity :cylon/identity}
-                      (get @store
-                           ;; I don't think this key has to include client-id
-                           ;; - it can just be 'code'.
-                           {:client-id client-id :code code})]
-
-               (let [{access-token :cylon.session/key}
-                     (create-session! (:access-token-store this) {:scopes #{:superuser/read-users :repo :superuser/gist :admin}})
-                     claim {:iss iss
-                            :sub identity
-                            :aud client-id
-                            :exp (plus (now) (days 1)) ; expiry
-                            :iat (now)}]
-
-                 (info "Claim is %s" claim)
-
-                 {:status 200
-                  :body (encode {"access_token" access-token
-                                 "scope" "repo gist openid profile email"
-                                 "token_type" "Bearer"
-                                 "expires_in" 3600
-                                 ;; TODO Refresh token (optional)
-                                 ;; ...
-                                 ;; OpenID Connect ID Token
-                                 "id_token" (-> claim
-                                                jwt
-                                                (sign :HS256 "secret") to-str)
-                                 })})
-               {:status 400
-                :body "You did not supply a valid code!"})))
-         wrap-params)})
+                 client-id (get params "client_id")
+                 client-secret (get params "client_secret")
+                 application (lookup-application+ (:application-registry this) client-id)]
+
+             (if (not= (:client-secret application) client-secret)
+               {:status 400 :body "Invalid request - bad secret"}
+
+               (if-let [{identity :cylon/identity}
+                        (get @store
+                             ;; I don't think this key has to include client-id
+                             ;; - it can just be 'code'.
+                             {:client-id client-id :code code})]
+
+                 (let [{access-token :cylon.session/key}
+                       (create-session! (:access-token-store this) {:scopes #{:superuser/read-users :repo :superuser/gist :admin}})
+                       claim {:iss iss
+                              :sub identity
+                              :aud client-id
+                              :exp (plus (now) (days 1)) ; expiry
+                              :iat (now)}]
+
+                   (info "Claim is %s" claim)
+
+                   {:status 200
+                    :body (encode {"access_token" access-token
+                                   "scope" "repo gist openid profile email"
+                                   "token_type" "Bearer"
+                                   "expires_in" 3600
+                                   ;; TODO Refresh token (optional)
+                                   ;; ...
+                                   ;; OpenID Connect ID Token
+                                   "id_token" (-> claim
+                                                  jwt
+                                                  (sign :HS256 "secret") to-str)
+                                   })})
+                 {:status 400
+                  :body "Invalid request - unknown code"}))))
+         wrap-params s/with-fn-validation)})
 
   (routes [this]
     ["/" {"authorize" {:get ::get-authenticate-form

                     {:status 302
                      :headers {"Location"
                                (format
                                 "http://localhost:8010/oauth/grant?code=%s&state=%s"
                                 code state
                                 )}
                      :cookies {"session-id" (->cookie session)}})))
               ;; Fail
               {:status 302
                :headers {"Location" (str "/login/oauth/authorize?client_id=" client-id)}
                :body "Try again"}
               )

             )
           )
         wrap-params wrap-cookies)

     ::get-totp-code
     (fn [req]

     ::post-totp-code
     (-> (fn [req]
           (let [code (-> req :form-params (get "code"))
                 secret (get-session-value req "session-id" (:session-store this) :totp-secret)
                 ]
             (if (= code (totp-token secret))
               ;; Success, set up the exchange
               (let [session (get-session (:session-store this) (get-cookie-value req "session-id"))
                     client-id (get session :client-id)
                     _ (infof "Looking up app with client-id %s yields %s" client-id (lookup-application+ (:application-registry this) client-id))
                     {:keys [callback-uri] :as application}
                     (lookup-application+ (:application-registry this) client-id)
                     state (get session :state)
                     identity (get session :cylon/identity)
                     code (str (java.util.UUID/randomUUID))]

                 ;; Remember the code for the possible exchange - TODO expire these
