140 lines
5.8 KiB
Org Mode
140 lines
5.8 KiB
Org Mode
#+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
|
|
#+begin_src lisp
|
|
(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.")
|
|
#+end_src
|
|
|
|
* 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
|