Skip to content

Instantly share code, notes, and snippets.

@jeroenvandijk
Last active March 22, 2024 17:52
Show Gist options
  • Save jeroenvandijk/ace7432be94d083e63729ac313a0b78f to your computer and use it in GitHub Desktop.
Save jeroenvandijk/ace7432be94d083e63729ac313a0b78f to your computer and use it in GitHub Desktop.
AWS SSO credential_process script
#!/usr/bin/env bb
;;;; USAGE
;; Add following entries to ~/.aws/credentials
;;
;; [production]
;; credential_process = bb tools/aws_sso_credentials.clj --region us-east-1 --account-id 1111111 --role-name Admin --startUrl https://your-start-url-hostname.awsapps.com/start
;;
;; [staging]
;; credential_process = bb tools/aws_sso_credentials.clj --region us-east-1 --account-id 2222222 --role-name SomeOtherRole --startUrl https://your-start-url-hostname.awsapps.com/start
;;
;;
;; More about this method to fetch aws sso credentials:
;; https://medium.com/@lex.berger/anatomy-of-aws-sso-device-authorization-grant-2839008c367a
(require
'[cheshire.core :as json]
'[clojure.tools.cli :refer [parse-opts]]
'[babashka.curl])
(def cli-options
[[nil "--role-name ROLE_NAME" "AWS role name"]
[nil "--account-id ACCOUNT_ID" "AWS account id"]
[nil "--region REGION" "AWS region Name"
:default (or (System/getenv "AWS_REGION") "us-east-1")]
[nil "--start-url START_URL" "Start url given by AWS SSO"]])
(defn aws-credentials [{:keys [role-name account-id]}]
(str (System/getenv "HOME") "/.aws/sso/credentials/" (str account-id "-" role-name)))
(defn expired-within? [inst seconds]
(neg? (- (.getSeconds (java.time.Duration/between (java.time.Instant/now) inst)) seconds)))
(defn http-request [{:keys [method headers body url json?] :as args}]
(let [f (case method
:post curl/post
:get curl/get)]
(let [{:keys [status] :as response} (f url (select-keys args [:headers :body]))]
(cond-> response
(and json? (<= 200 status 299))
(update :body #(json/parse-string % true))))))
(defn api-url [{:keys [region]} path]
(str "https://oidc." region ".amazonaws.com" path))
(defn get-new-client [client-name opts]
(:body (http-request {:method :post
:url (api-url opts "/client/register")
:headers {"Content-type" "application/json"}
:json? true
:body (json/generate-string {
"clientName" client-name
"clientType" "public",})})))
(defn get-client [client-name {:keys [role-name role region account-id] :as opts}]
(let [path (str (System/getenv "HOME") "/.aws/sso/cache/" client-name ".json")
f (java.io.File. path)
act (fn []
(let [client (get-new-client client-name opts)]
(future
(clojure.java.io/make-parents f)
(spit f (json/generate-string client)))
client))]
(if (.exists f)
(let [{:keys [clientSecretExpiresAt] :as creds} (json/parse-string (slurp f) true)]
(if (expired-within? (java.time.Instant/ofEpochSecond clientSecretExpiresAt) 60)
(act)
creds))
(act))))
(defn register-device [client {:keys [start-url] :as opts}]
(:body (http-request {:method :post
:url (api-url opts "/device_authorization")
:headers {"Content-type" "application/json"}
:json? true
:body
(json/generate-string
(assoc (select-keys client [:clientId :clientSecret])
:startUrl start-url))})))
(defn try-access-token [client device opts]
(try (http-request {:method :post
:url (api-url opts "/token")
:headers {"Content-type" "application/json"}
:json? true
:body
(json/generate-string
(assoc (select-keys client [:clientId :clientSecret])
"deviceCode" (:deviceCode device)
"grantType" "urn:ietf:params:oauth:grant-type:device_code"))})
(catch Exception e e)))
(defn complete-verification [device]
;;; Interactively open verification complete
(let [res (clojure.java.shell/sh "open" (:verificationUriComplete device))]
(zero? (:exit res))))
(defn error-with [msg]
(println msg)
(System/exit 1))
(defn get-access-token [client device opts]
(if-not (complete-verification device)
(error-with "error complete device" )
(loop [i 0]
(if (= i 150)
(error-with "no response with 30 seconds" )
(do
(Thread/sleep (case i 200))
; (print ".") (flush)
(let [response (try-access-token client device opts)]
(if (= (:status response) 200)
(get-in response [:body :accessToken])
(recur (inc i)))))))))
(defn get-role-credentials [{:keys [access-token region account-id role-name]}]
(-> (http-request {:method :get
:url (format "https://portal.sso.%s.amazonaws.com/federation/credentials?account_id=%s&role_name=%s" region account-id role-name)
:headers {"x-amz-sso_bearer_token" access-token}
:json? true})
(get-in [:body :roleCredentials])
(clojure.set/rename-keys {:accessKeyId :AccessKeyId
:secretAccessKey :SecretAccessKey
:sessionToken :SessionToken
:expiration :Expiration})
(update :Expiration (fn [ts] (str (java.time.Instant/ofEpochSecond (/ ts 1000)))))))
(defn get-sso-credentials [{:keys [role-name region account-id start-url] :as opts}]
(let [path (aws-credentials opts)
client (get-client "foobar" opts)
device (register-device client opts)
access-token (get-access-token client device opts)]
(get-role-credentials {:access-token access-token
:region region
:account-id account-id
:role-name role-name})))
(defn get-credentials [{:keys [role-name role region account-id] :as opts}]
(let [path (aws-credentials opts)
f (java.io.File. path)
act (fn []
(let [creds-str (-> (get-sso-credentials opts)
(assoc :Version 1)
(json/generate-string))]
(do #_future ;; FIXME Something weird, prints empty string to file
(clojure.java.io/make-parents f)
(spit f creds-str))
(println creds-str)))]
(if (.exists f)
(let [creds-str (slurp f)
{:keys [Expiration] :as creds} (json/parse-string creds-str true)]
(if (or (not Expiration)
(expired-within? (java.time.Instant/parse Expiration) 60))
(act)
(println creds-str)))
(act))))
(defn -main [args]
(let [{:keys [options arguments errors summary]} (parse-opts args cli-options)
{:keys [role-name account-id region start-url]} options]
(when-not (and role-name account-id region start-url)
(println "--role-name, --account-id, --start-url or --region missing")
(System/exit 1))
(get-credentials options)))
;; bb aws_sso_credentials.clj --account-id 1111111 --role-name AdministratorAccess --region us-east-1 --start-url "https://<your-url>.awsapps.com/start"
(-main *command-line-args* )
(comment
(def account-id 1111111)
(def role-name "AdministratorAccess")
(def region "us-east-1")
(def opts {:region region
:start-url "https://<your-url>.awsapps.com/start"
:account-id account-id
:role-name role-name})
(def client (get-new-client "foobar" opts))
(def client (get-client "foobar" opts))
(def startUrl "https://d-90676580c1.awsapps.com/start")
(def device (register-device client opts))
;;; Interactively open verification complete
(clojure.java.shell/sh "open" (:verificationUriComplete device))
(try-access-token client device opts)
(def access-token (get-access-token client device opts))
(get-role-credentials {:access-token access-token
:region region
:account-id account-id
:role-name role-name})
(def creds (get-role-credentials {:access-token access-token
:region region
:account-id account-id
:role-name role-name}))
(get-role-credentials {:access-token access-token
:region region
:account-id account-id
:role-name role-name})
(get-sso-credentials {:region region
:account-id account-id
:role-name role-name})
(get-credentials {:region region
:account-id account-id
:role-name role-name})
)
@jjttjj
Copy link

jjttjj commented Mar 21, 2024

Hey, this is a hidden gem! Any chance you have it open sourced somewhere, or would you be willing to stick a license on it? I've adapted it for my own usage with a few changes

@jeroenvandijk
Copy link
Author

Hey @jjttjj that's very kind of you. If you want to use this, it would be licensed under the EPL-1.0 license.

However, I did create an improved version of the above, but I didn't open source it until now: https://github.com/jeroenvandijk/aws.console

It has some nice extra features :) Let me know if you need help to set it up.

@jjttjj
Copy link

jjttjj commented Mar 22, 2024

Awesome, thanks for that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment