-
-
Save jeroenvandijk/ace7432be94d083e63729ac313a0b78f to your computer and use it in GitHub Desktop.
AWS SSO credential_process script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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}) | |
) |
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.
Awesome, thanks for that!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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