Files
memex/notes/org-skill-auth-google-oauth.org

5.8 KiB

SKILL: Google OAuth 2.0 Authentication (Universal Literate Note)

Overview

This skill implements the Headless OAuth 2.0 handshake for Google services. It enables the agent to acquire and rotate `access_tokens` for Gemini without requiring a local browser session, using a "Copy-Paste" authorization code flow.

Phase A: Demand (PRD)

1. Purpose

Provide a secure, professional OAuth 2.0 interface for Google Gemini.

2. User Needs

  • Headless Handshake: Generate an Auth URL and accept a pasted Code from the user.
  • Token Persistence: Securely store `refresh_token` in a Lisp state file.
  • Auto-Rotation: Automatically exchange the `refresh_token` for a new `access_token` when expired.
  • Environment Driven: Pull `CLIENT_ID` and `CLIENT_SECRET` from system settings.

3. Success Criteria

TODO Generate valid Google OAuth Authorization URL

TODO Exchange Authorization Code for Token Plist

TODO Persist and Retrieve tokens from auth-google.lisp

TODO Automated Token Refresh Loop

Phase B: Blueprint (PROTOCOL)

1. Architectural Intent

Interfaces for the OAuth lifecycle. Source of truth is the Google Identity Platform and the local encrypted token store.

2. Semantic Interfaces

(defun auth-google-get-url ()
  "Generates the URL for the user to visit in their browser.")

(defun auth-google-receive-code (code)
  "Exchanges the manual code for tokens and persists them.")

(defun auth-google-get-header ()
  "Returns the Bearer token header, refreshing if necessary.")

Phase D: Build (Implementation)

(defvar *google-token-state* nil)

(defun auth-google-load-state ()
  (let ((state-file (merge-pathnames "state/auth-google.lisp" (uiop:getenv "SYSTEM_DIR"))))
    (if (uiop:file-exists-p state-file)
        (setf *google-token-state* (with-open-file (in state-file) (read in)))
        (setf *google-token-state* nil))))

(defun auth-google-save-state ()
  (let* ((state-dir (uiop:getenv "SYSTEM_DIR"))
         (state-file (merge-pathnames "state/auth-google.lisp" state-dir)))
    (ensure-directories-exist state-file)
    (with-open-file (out state-file :direction :output :if-exists :supersede)
      (print *google-token-state* out))))

(defun auth-google-receive-code (code)
  "Exchanges the manual authorization code for access and refresh tokens."
  (let ((url "https://oauth2.googleapis.com/token")
        (content `(("code" . ,code)
                   ("client_id" . ,(uiop:getenv "GOOGLE_CLIENT_ID"))
                   ("client_secret" . ,(uiop:getenv "GOOGLE_CLIENT_SECRET"))
                   ("redirect_uri" . "urn:ietf:wg:oauth:2.0:oob")
                   ("grant_type" . "authorization_code"))))
    (handler-case
        (let* ((response (dex:post url :content content))
               (json (cl-json:decode-json-from-string response)))
          (setf *google-token-state* 
                `(:access-token ,(cdr (assoc :access--token json))
                  :refresh-token ,(cdr (assoc :refresh--token json))
                  :expires-at ,(+ (get-universal-time) (cdr (assoc :expires--in json)))))
          (auth-google-save-state)
          (kernel-log "OAUTH - Google handshake successful.")
          t)
      (error (c)
        (kernel-log "OAUTH ERROR - Handshake failed: ~a" c)
        nil))))

(defun auth-google-refresh-token ()
  "Uses the refresh_token to acquire a new access_token."
  (let ((refresh-token (getf *google-token-state* :refresh-token))
        (url "https://oauth2.googleapis.com/token")
        (content `(("refresh_token" . ,(getf *google-token-state* :refresh-token))
                   ("client_id" . ,(uiop:getenv "GOOGLE_CLIENT_ID"))
                   ("client_secret" . ,(uiop:getenv "GOOGLE_CLIENT_SECRET"))
                   ("grant_type" . "refresh_token"))))
    (unless refresh-token (return-from auth-google-refresh-token nil))
    (handler-case
        (let* ((response (dex:post url :content content))
               (json (cl-json:decode-json-from-string response)))
          (setf (getf *google-token-state* :access-token) (cdr (assoc :access--token json)))
          (setf (getf *google-token-state* :expires-at) (+ (get-universal-time) (cdr (assoc :expires--in json))))
          (auth-google-save-state)
          (kernel-log "OAUTH - Google token refreshed.")
          t)
      (error (c)
        (kernel-log "OAUTH ERROR - Refresh failed: ~a" c)
        nil))))

(defun auth-google-get-header ()
  "Returns the Bearer token header, refreshing if necessary."
  (unless *google-token-state* (auth-google-load-state))
  (let ((expires-at (getf *google-token-state* :expires-at 0)))
    (when (<= expires-at (+ (get-universal-time) 60)) ; Refresh if < 1 min left
      (auth-google-refresh-token)))
  (let ((token (getf *google-token-state* :access-token)))
    (if token
        (list :bearer-token token)
        (progn
          (kernel-log "OAUTH - No active Google token. Handshake required.")
          (kernel-log "OAUTH - Visit this URL: ~a" (auth-google-get-url))
          nil))))

(defun auth-google-get-url ()
  (let ((client-id (uiop:getenv "GOOGLE_CLIENT_ID")))
    (format nil "https://accounts.google.com/o/oauth2/v2/auth?client_id=~a&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/generative-language" client-id)))

;; Register as the primary auth provider for Gemini
(org-agent:register-auth-provider :gemini #'auth-google-get-header)

Registration

(defskill :skill-auth-google-oauth
  :priority 100
  :trigger (lambda (context) nil)
  :neuro (lambda (context) nil)
  :symbolic (lambda (action context) action))