this repo has no description smallweb.run
smallweb
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

add openauth openid adapter

pomdtr 33cc7bf1 8a534a68

+165 -46
+88 -40
cmd/up.go
··· 11 11 "io" 12 12 "net" 13 13 "net/http" 14 + "net/url" 14 15 "os" 15 16 "os/exec" 16 17 "os/signal" ··· 80 81 workers: make(map[string]*worker.Worker), 81 82 } 82 83 83 - if issuer := k.String("oidc.issuer"); issuer != "" { 84 - provider, err := oidc.NewProvider(context.Background(), issuer) 85 - if err != nil { 86 - return fmt.Errorf("failed to get provider: %v", err) 87 - } 88 - 89 - handler.oidcProvider = provider 84 + issuerUrl, err := url.Parse(k.String("oidc.issuer")) 85 + if err != nil { 86 + return fmt.Errorf("failed to parse issuer url: %v", err) 90 87 } 88 + handler.oidcIssuerUrl = issuerUrl 91 89 92 90 watcher, err := watcher.NewWatcher(k.String("dir"), func() { 93 91 fileProvider := file.Provider(utils.FindConfigPath(k.String("dir"))) ··· 99 97 _ = k.Load(flagProvider, nil) 100 98 101 99 if issuer := k.String("oidc.issuer"); issuer != "" { 102 - provider, err := oidc.NewProvider(context.Background(), issuer) 100 + issuerUrl, err := url.Parse(issuer) 103 101 if err != nil { 104 - fmt.Fprintf(cmd.ErrOrStderr(), "failed to get provider: %v\n", err) 102 + fmt.Fprintf(cmd.ErrOrStderr(), "failed to parse issuer url: %v\n", err) 105 103 return 106 104 } 107 105 108 - handler.oidcProvider = provider 106 + handler.oidcIssuerUrl = issuerUrl 109 107 } 110 108 }) 111 109 if err != nil { ··· 434 432 } 435 433 436 434 type Handler struct { 437 - watcher *watcher.Watcher 438 - mu sync.Mutex 439 - workers map[string]*worker.Worker 440 - oidcProvider *oidc.Provider 435 + watcher *watcher.Watcher 436 + workerMu sync.Mutex 437 + workers map[string]*worker.Worker 438 + oidcMu sync.Mutex 439 + oidcIssuerUrl *url.URL 440 + oidcProvider *oidc.Provider 441 441 } 442 442 443 443 type AuthData struct { ··· 477 477 return 478 478 } 479 479 480 + if me.oidcIssuerUrl != nil && me.oidcIssuerUrl.Host == r.Host { 481 + wk, err := me.GetWorker(appname, k.String("dir"), k.String("domain")) 482 + if err != nil { 483 + if errors.Is(err, app.ErrAppNotFound) { 484 + w.WriteHeader(http.StatusNotFound) 485 + w.Write([]byte(fmt.Sprintf("No app found for host %s", r.Host))) 486 + return 487 + } 488 + 489 + w.WriteHeader(http.StatusInternalServerError) 490 + fmt.Fprintf(w, "failed to get worker: %v", err) 491 + return 492 + } 493 + 494 + wk.ServeHTTP(w, r) 495 + return 496 + } 497 + 480 498 if r.URL.Path == "/_smallweb/signin" { 481 499 oauth2Config, err := me.Oauth2Config(r.Host) 482 500 if err != nil { ··· 589 607 return 590 608 } 591 609 592 - oauth2Token, err := oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(authData.CodeVerifier)) 610 + code := r.URL.Query().Get("code") 611 + if code == "" { 612 + http.Error(w, "oauth code not found", http.StatusUnauthorized) 613 + return 614 + } 615 + 616 + oauth2Token, err := oauth2Config.Exchange(r.Context(), code, oauth2.VerifierOption(authData.CodeVerifier)) 593 617 if err != nil { 594 618 http.Error(w, fmt.Sprintf("failed to exchange code: %v", err), http.StatusInternalServerError) 595 619 return 596 620 } 597 621 598 - idToken := oauth2Token.Extra("id_token").(string) 599 - if idToken == "" { 622 + idToken, ok := oauth2Token.Extra("id_token").(string) 623 + if !ok { 600 624 http.Error(w, "id token not found", http.StatusInternalServerError) 601 625 return 602 626 } ··· 622 646 MaxAge: 34560000, 623 647 }) 624 648 } 649 + 650 + http.Redirect(w, r, authData.SuccessURL, http.StatusTemporaryRedirect) 651 + return 625 652 } 626 653 627 - userinfos, err := me.extractUserInfos(r) 654 + claims, err := me.extractClaims(r) 628 655 if err != nil && isRoutePrivate(appname, r.URL.Path) { 629 656 if !errors.Is(err, &oidc.TokenExpiredError{}) { 630 657 http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) ··· 658 685 return 659 686 } 660 687 661 - verifier := me.oidcProvider.Verifier(&oidc.Config{ClientID: r.Host}) 688 + if me.Provider() == nil { 689 + http.Error(w, "oidc provider not found", http.StatusInternalServerError) 690 + return 691 + } 692 + 693 + verifier := me.Provider().Verifier(&oidc.Config{ClientID: r.Host}) 662 694 idToken, err := verifier.Verify(r.Context(), rawIdToken) 663 695 if err != nil { 664 696 http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 665 697 return 666 698 } 667 699 668 - if err := idToken.Claims(&userinfos); err != nil { 700 + if err := idToken.Claims(&claims); err != nil { 669 701 http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 670 702 return 671 703 } ··· 694 726 } 695 727 } 696 728 697 - if isRoutePrivate(appname, r.URL.Path) && !isAuthorized(appname, userinfos.Email, userinfos.Group) { 698 - if userinfos.Email == "" { 729 + if isRoutePrivate(appname, r.URL.Path) && !isAuthorized(appname, claims.Email, claims.Group) { 730 + if claims.Email == "" { 699 731 http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 700 732 return 701 733 } ··· 704 736 return 705 737 } 706 738 707 - r.Header.Set("Remote-User", userinfos.User) 708 - r.Header.Set("Remote-Email", userinfos.Email) 709 - r.Header.Set("Remote-Group", userinfos.Group) 710 - r.Header.Set("Remote-Name", userinfos.Name) 739 + r.Header.Set("Remote-User", claims.User) 740 + r.Header.Set("Remote-Email", claims.Email) 741 + r.Header.Set("Remote-Group", claims.Group) 742 + r.Header.Set("Remote-Name", claims.Name) 711 743 712 744 wk, err := me.GetWorker(appname, k.String("dir"), k.String("domain")) 713 745 if err != nil { ··· 755 787 return slices.Contains(authorizedEmails, email) || slices.Contains(authorizedGroups, group) 756 788 } 757 789 758 - type UserInfos struct { 790 + type Claims struct { 759 791 Email string 760 792 Group string 761 793 User string 762 794 Name string 763 795 } 764 796 765 - func (me *Handler) extractUserInfos(r *http.Request) (UserInfos, error) { 766 - if me.oidcProvider == nil { 767 - return UserInfos{ 797 + func (me *Handler) extractClaims(r *http.Request) (Claims, error) { 798 + if me.Provider() == nil { 799 + return Claims{ 768 800 Email: r.Header.Get("Remote-Email"), 769 801 Group: r.Header.Get("Remote-Group"), 770 802 User: r.Header.Get("Remote-User"), ··· 774 806 775 807 idTokenCookie, err := r.Cookie("id_token") 776 808 if err != nil { 777 - return UserInfos{}, fmt.Errorf("id token not found") 809 + return Claims{}, fmt.Errorf("id token not found") 778 810 } 779 811 780 - verifier := me.oidcProvider.Verifier(&oidc.Config{ClientID: fmt.Sprintf("https://%s", r.Host)}) 812 + verifier := me.Provider().Verifier(&oidc.Config{ClientID: fmt.Sprintf("https://%s", r.Host)}) 781 813 idToken, err := verifier.Verify(r.Context(), idTokenCookie.Value) 782 814 if err != nil { 783 - return UserInfos{}, fmt.Errorf("failed to verify id token: %v", err) 815 + return Claims{}, fmt.Errorf("failed to verify id token: %v", err) 784 816 } 785 817 786 - var userinfo UserInfos 818 + var userinfo Claims 787 819 if err := idToken.Claims(&userinfo); err != nil { 788 - return UserInfos{}, fmt.Errorf("failed to extract claims: %v", err) 820 + return Claims{}, fmt.Errorf("failed to extract claims: %v", err) 789 821 } 790 822 791 823 return userinfo, nil ··· 819 851 return "", false, false 820 852 } 821 853 822 - func (me *Handler) Oauth2Config(host string) (*oauth2.Config, error) { 854 + func (me *Handler) Provider() *oidc.Provider { 855 + me.oidcMu.Lock() 856 + defer me.oidcMu.Unlock() 857 + 858 + if me.oidcIssuerUrl == nil { 859 + return nil 860 + } 861 + 823 862 if me.oidcProvider == nil { 824 - return nil, fmt.Errorf("oidc provider not found") 863 + provider, err := oidc.NewProvider(context.Background(), me.oidcIssuerUrl.String()) 864 + if err != nil { 865 + return nil 866 + } 867 + 868 + me.oidcProvider = provider 825 869 } 826 870 871 + return me.oidcProvider 872 + } 873 + 874 + func (me *Handler) Oauth2Config(host string) (*oauth2.Config, error) { 827 875 clientID := fmt.Sprintf("https://%s", host) 828 876 return &oauth2.Config{ 829 877 ClientID: clientID, 830 878 Scopes: []string{"openid", "email", "profile", "groups"}, 831 879 RedirectURL: fmt.Sprintf("https://%s/_smallweb/oauth/callback", host), 832 - Endpoint: me.oidcProvider.Endpoint(), 880 + Endpoint: me.Provider().Endpoint(), 833 881 }, nil 834 882 } 835 883 ··· 838 886 return wk, nil 839 887 } 840 888 841 - me.mu.Lock() 842 - defer me.mu.Unlock() 889 + me.workerMu.Lock() 890 + defer me.workerMu.Unlock() 843 891 844 892 a, err := app.LoadApp(appname, k.String("dir"), k.String("domain"), k.Bool(fmt.Sprintf("apps.%s.admin", appname))) 845 893 if err != nil {
+1 -1
example/.smallweb/config.json
··· 1 1 { 2 2 "$schema": "../../schemas/config.schema.json", 3 3 "domain": "smallweb.localhost", 4 - "oidc.issuer": "https://lastlogin.net", 4 + "oidc.issuer": "https://auth.smallweb.localhost", 5 5 "authorizedEmails": [ 6 6 "achille.lacoin@gmail.com" 7 7 ],
+76 -5
example/auth/main.ts
··· 5 5 import { MemoryStorage } from "npm:/@openauthjs/openauth@^0.3.7/storage/memory"; 6 6 import { createSubjects } from "npm:@openauthjs/openauth@^0.3.7/subject"; 7 7 import { object, string } from "npm:valibot@1.0.0" 8 + import { signingKeys } from "npm:@openauthjs/openauth@^0.3.7/keys"; 9 + import { jwtVerify, SignJWT } from "npm:jose" 8 10 import * as fs from "jsr:@std/fs@^1.0.11"; 9 11 10 12 const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = Deno.env.toObject(); ··· 12 14 throw new Error("Missing GITHUB_CLIENT_ID or GITHUB_CLIENT_SECRET"); 13 15 } 14 16 15 - await fs.ensureDir("./data"); 17 + const storage = MemoryStorage({ 18 + persist: "./data/db.json", 19 + }) 16 20 17 - export default issuer({ 21 + const iss = issuer({ 18 22 theme: THEME_SST, 19 23 providers: { 20 24 github: GithubProvider({ ··· 23 27 scopes: ["user:email"], 24 28 }), 25 29 }, 26 - storage: MemoryStorage({ 27 - persist: "./data/db.json", 28 - }), 30 + storage, 29 31 subjects: createSubjects({ 30 32 user: object({ 31 33 email: string(), ··· 53 55 }) 54 56 55 57 58 + export default { 59 + fetch: async (req: Request) => { 60 + await fs.ensureDir("./data"); 61 + const url = new URL(req.url); 62 + 63 + if (url.pathname === "/.well-known/openid-configuration") { 64 + const resp = await iss.request(new URL("/.well-known/oauth-authorization-server", url)) 65 + const oauth2Config = await resp.json() 66 + return Response.json({ 67 + ...oauth2Config, 68 + userinfo_endpoint: new URL("/userinfo", url).toString(), 69 + scopes_supported: ["openid", "email"], 70 + id_token_signing_alg_values_supported: ["ES256"], 71 + }) 72 + } 73 + 74 + if (url.pathname === "/token") { 75 + if (req.headers.get("content-type") !== "application/x-www-form-urlencoded") { 76 + return new Response("Invalid content type", { 77 + status: 400, 78 + }) 79 + } 80 + 81 + const params = new URLSearchParams(await req.text()) 82 + if (!params.has("client_id")) { 83 + return new Response("Missing client_id", { 84 + status: 400, 85 + }) 86 + } 87 + 88 + const resp = await iss.request(req.url, { 89 + method: req.method, 90 + headers: req.headers, 91 + body: params.toString(), 92 + }) 93 + 94 + if (!resp.ok) { 95 + return resp 96 + } 97 + 98 + const tokens = await resp.json() 99 + 100 + const signinKey = await signingKeys(storage).then((keys) => keys[0]) 101 + const access_token = await jwtVerify<{ 102 + properties: { 103 + email: string 104 + } 105 + }>(tokens.access_token, signinKey.public) 106 + const jwt = new SignJWT({ 107 + aud: access_token.payload.aud, 108 + iss: access_token.payload.iss, 109 + sub: access_token.payload.sub, 110 + exp: access_token.payload.exp, 111 + email: access_token.payload.properties.email, 112 + }) 113 + 114 + jwt.setProtectedHeader(access_token.protectedHeader) 115 + jwt.sign(signinKey.private) 116 + 117 + 118 + return Response.json({ 119 + id_token: await jwt.sign(signinKey.private), 120 + ...tokens, 121 + }) 122 + } 123 + 124 + return iss.fetch(req); 125 + } 126 + }