[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/ement 03d411a177: Add: SSO support
From: |
ELPA Syncer |
Subject: |
[elpa] externals/ement 03d411a177: Add: SSO support |
Date: |
Tue, 30 May 2023 03:57:48 -0400 (EDT) |
branch: externals/ement
commit 03d411a1778179dd1b68b404059e84508a2c1fd8
Author: Adam Porter <adam@alphapapa.net>
Commit: Adam Porter <adam@alphapapa.net>
Add: SSO support
Closes <https://github.com/alphapapa/ement.el/issues/24>.
Co-developed-by: Jeffrey Stoffers <jstoffers@uberpurple.com>
---
README.org | 3 ++
ement.el | 164 +++++++++++++++++++++++++++++++++++++++----------------------
2 files changed, 107 insertions(+), 60 deletions(-)
diff --git a/README.org b/README.org
index 6397dbcdc6..1570f19c08 100644
--- a/README.org
+++ b/README.org
@@ -298,6 +298,9 @@ Note that, while ~matrix-client~ remains usable, and
probably will for some time
** 0.10-pre
+*Additions*
++ Support for Single Sign-On (SSO) authentication.
([[https://github.com/alphapapa/ement.el/issues/24][#24]]. Thanks to
[[https://github.com/Necronian][Jeffrey Stoffers]] for development, and to
[[https://github.com/phil-s][Phil Sainty]], [[https://github.com/FrostyX][Jakub
Kadlčík]], and [[https://github.com/oneingan][Juanjo Presa]] for testing.)
+
*Changes*
+ Activating a space in the room list uses ~ement-view-space~ (which shows a
directory of rooms in the space) instead of ~ement-view-room~ (which shows
events in the space, which is generally not useful).
diff --git a/ement.el b/ement.el
index aed01dfd13..10b958021b 100644
--- a/ement.el
+++ b/ement.el
@@ -184,6 +184,11 @@ handled (e.g. how to be notified)."
:type 'hook
:options '(ement-interrupted-sync-message ement-interrupted-sync-warning))
+(defcustom ement-sso-server-port 4567
+ "TCP port used for local HTTP server for SSO logins.
+It shouldn't usually be necessary to change this."
+ :type 'integer)
+
;;;; Commands
;;;###autoload
@@ -205,8 +210,7 @@ the port, e.g.
\"http://localhost:8080\""
(interactive (if current-prefix-arg
;; Force new session.
- (list :user-id (read-string "User ID: ")
- :password (read-passwd "Password: "))
+ (list :user-id (read-string "User ID: "))
;; Use known session.
(unless ement-sessions
;; Read sessions from disk.
@@ -215,66 +219,106 @@ the port, e.g.
(error (display-warning 'ement (format "Unable to read
session data from disk (%s). Prompting to log in again."
(error-message-string err))))))
(cl-case (length ement-sessions)
- (0 (list :user-id (read-string "User ID: ")
- :password (read-passwd "Password: ")))
+ (0 (list :user-id (read-string "User ID: ")))
(1 (list :session (cdar ement-sessions)))
(otherwise (list :session (ement-complete-session))))))
- (cl-labels ((new-session
- () (unless (string-match (rx bos "@" (group (1+ (not (any
":")))) ; Username
- ":" (group (optional (1+ (not (any
blank)))))) ; Server name
- user-id)
- (user-error "Invalid user ID format: use
@USERNAME:SERVER"))
- (let* ((username (match-string 1 user-id))
- (server-name (match-string 2 user-id))
- (uri-prefix (or uri-prefix (ement--hostname-uri
server-name)))
- (user (make-ement-user :id user-id :username username))
- (server (make-ement-server :name server-name :uri-prefix
uri-prefix))
- (transaction-id (ement--initial-transaction-id))
- (initial-device-display-name (format "Ement.el: %s@%s"
- ;; Just to be extra
careful:
- (or user-login-name
"[unknown user-login-name]")
- (or (system-name)
"[unknown system-name]")))
- (device-id (secure-hash 'sha256
initial-device-display-name)))
- (make-ement-session :user user :server server :transaction-id
transaction-id
- :device-id device-id
:initial-device-display-name initial-device-display-name
- :events (make-hash-table :test #'equal))))
- (password-login
- () (pcase-let* (((cl-struct ement-session user device-id
initial-device-display-name) session)
- ((cl-struct ement-user id) user)
- (data (ement-alist "type" "m.login.password"
- "identifier"
- (ement-alist "type"
"m.id.user"
- "user" id)
- "password" password
- "device_id" device-id
-
"initial_device_display_name" initial-device-display-name)))
- ;; TODO: Clear password in callback (if we decide to hold
on to it for retrying login timeouts).
- (ement-api session "login" :method 'post :data
(json-encode data)
- :then (apply-partially #'ement--login-callback
session))))
- (flows-callback
- (data) (if (cl-loop for flow across (map-elt data 'flows)
- thereis (equal (map-elt flow 'type)
"m.login.password"))
- (progn
- (message "Ement: Logging in with password...")
- (password-login))
- (error "Matrix server doesn't support m.login.password
login flow. Supported flows: %s"
- (cl-loop for flow in (map-elt data 'flows)
- collect (map-elt flow 'type))))))
- (if session
- ;; Start syncing given session.
- (let ((user-id (ement-user-id (ement-session-user session))))
- ;; HACK: If session is already in ement-sessions, this replaces it.
I think that's okay...
- (setf (alist-get user-id ement-sessions nil nil #'equal) session)
- (ement--sync session :timeout ement-initial-sync-timeout))
- ;; Start password login flow. Prompt for user ID and password
- ;; if not given (i.e. if not called interactively.)
- (unless user-id
- (setf user-id (read-string "User ID: ")))
- (unless password
- (setf password (read-passwd (format "Password for %s: " user-id))))
- (setf session (new-session))
- (when (ement-api session "login" :then #'flows-callback)
- (message "Ement: Checking server's login flows...")))))
+ (let (sso-server-process)
+ (cl-labels ((new-session
+ () (unless (string-match (rx bos "@" (group (1+ (not (any
":")))) ; Username
+ ":" (group (optional (1+ (not
(any blank)))))) ; Server name
+ user-id)
+ (user-error "Invalid user ID format: use
@USERNAME:SERVER"))
+ (let* ((username (match-string 1 user-id))
+ (server-name (match-string 2 user-id))
+ (uri-prefix (or uri-prefix (ement--hostname-uri
server-name)))
+ (user (make-ement-user :id user-id :username username))
+ (server (make-ement-server :name server-name
:uri-prefix uri-prefix))
+ (transaction-id (ement--initial-transaction-id))
+ (initial-device-display-name (format "Ement.el: %s@%s"
+ ;; Just to be
extra careful:
+ (or
user-login-name "[unknown user-login-name]")
+ (or (system-name)
"[unknown system-name]")))
+ (device-id (secure-hash 'sha256
initial-device-display-name)))
+ (make-ement-session :user user :server server
:transaction-id transaction-id
+ :device-id device-id
:initial-device-display-name initial-device-display-name
+ :events (make-hash-table :test
#'equal))))
+ (password-login
+ () (pcase-let* (((cl-struct ement-session user device-id
initial-device-display-name) session)
+ ((cl-struct ement-user id) user)
+ (data (ement-alist "type" "m.login.password"
+ "identifier"
+ (ement-alist "type"
"m.id.user"
+ "user" id)
+ "password" (or password
+
(read-passwd (format "Password for %s: " id)))
+ "device_id" device-id
+
"initial_device_display_name" initial-device-display-name)))
+ ;; TODO: Clear password in callback (if we decide to
hold on to it for retrying login timeouts).
+ (ement-api session "login" :method 'post :data
(json-encode data)
+ :then (apply-partially #'ement--login-callback
session))
+ (ement-message "Logging in with password...")))
+ (sso-filter
+ (process string)
+ ;; NOTE: This is technically wrong, because it's not
guaranteed that the
+ ;; string will be a complete request--it could just be a
chunk. But in
+ ;; practice, if this works, it's much simpler than setting up
process log
+ ;; functions and per-client buffers for this throwaway,
pretend HTTP server.
+ (when (string-match (rx "GET /?loginToken=" (group (0+ nonl))
" " (0+ nonl)) string)
+ (unwind-protect
+ (pcase-let* ((token (match-string 1 string))
+ ((cl-struct ement-session user device-id
initial-device-display-name)
+ session)
+ ((cl-struct ement-user id) user)
+ (data (ement-alist
+ "type" "m.login.token"
+ "identifier" (ement-alist "type"
"m.id.user"
+ "user" id)
+ "token" token
+ "device_id" device-id
+ "initial_device_display_name"
initial-device-display-name)))
+ (ement-api session "login" :method 'post
+ :data (json-encode data)
+ :then (apply-partially #'ement--login-callback
session)))
+ (delete-process sso-server-process)
+ (delete-process process))))
+ (sso-login ()
+ (setf sso-server-process
+ (make-network-process
+ :name "ement-sso" :family 'ipv4 :host 'local
:service ement-sso-server-port
+ :filter #'sso-filter :server t :noquery t))
+ (browse-url
+ (concat (ement-server-uri-prefix
(ement-session-server session))
+
"/_matrix/client/r0/login/sso/redirect?redirectUrl=http://localhost:"
+ (number-to-string ement-sso-server-port))))
+ (flows-callback
+ (data) (let ((flows (cl-loop for flow across (map-elt data
'flows)
+ collect (map-elt flow 'type))))
+ (pcase (length flows)
+ (1 (pcase (car flows)
+ ("m.login.password" (password-login))
+ ("m.login.sso" (sso-login))
+ (_ (error "Ement: Unsupported login flow: %s
Server:%s"
+ (car flows)
(ement-server-uri-prefix (ement-session-server session))))))
+ (_ (pcase (completing-read "Select authentication
method: "
+ (cl-loop for flow in
flows
+ collect
(string-trim-left flow (rx "m.login."))))
+ ("password" (password-login))
+ ("sso" (sso-login))
+ (else (error "Ement: Unsupported login
flow:%S Server:%S Supported flows:%S"
+ else (ement-server-uri-prefix
(ement-session-server session)) flows))))))))
+ (if session
+ ;; Start syncing given session.
+ (let ((user-id (ement-user-id (ement-session-user session))))
+ ;; HACK: If session is already in ement-sessions, this replaces
it. I think that's okay...
+ (setf (alist-get user-id ement-sessions nil nil #'equal) session)
+ (ement--sync session :timeout ement-initial-sync-timeout))
+ ;; Start password login flow. Prompt for user ID and password
+ ;; if not given (i.e. if not called interactively.)
+ (unless user-id
+ (setf user-id (read-string "User ID: ")))
+ (setf session (new-session))
+ (when (ement-api session "login" :then #'flows-callback)
+ (message "Ement: Checking server's login flows..."))))))
(defun ement-disconnect (sessions)
"Disconnect from SESSIONS.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [elpa] externals/ement 03d411a177: Add: SSO support,
ELPA Syncer <=