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

5.7 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

"Generates the URL for the user to visit in their browser." "Exchanges the manual code for tokens and persists them." "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))