#+TITLE: SKILL: Google OAuth 2.0 Authentication (Universal Literate Note) #+ID: skill-auth-google-oauth #+STARTUP: content #+FILETAGS: :auth:oauth:google:security:psf: * 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) :PROPERTIES: :STATUS: FROZEN :END: ** 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) :PROPERTIES: :STATUS: SIGNED :END: ** 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) #+begin_src lisp :tangle ../projects/org-skill-auth-google-oauth/src/auth-google-oauth.lisp (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) #+end_src * Registration #+begin_src lisp (defskill :skill-auth-google-oauth :priority 100 :trigger (lambda (context) nil) :neuro (lambda (context) nil) :symbolic (lambda (action context) action)) #+end_src