this repo has no description smallweb.run
smallweb
4
fork

Configure Feed

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

improve handling of authentication

pomdtr 5fab92b8 a5b5b3b3

+212 -180
+212 -180
cmd/up.go
··· 477 477 return 478 478 } 479 479 480 - isRouteProtected := func(route string) bool { 481 - routeisProtected := k.Bool(fmt.Sprintf("apps.%s.private", appname)) 482 - for _, publicRoute := range k.Strings(fmt.Sprintf("apps.%s.publicRoutes", appname)) { 483 - if isMatch, _ := doublestar.Match(publicRoute, route); isMatch { 484 - routeisProtected = false 485 - break 486 - } 480 + if r.URL.Path == "/_smallweb/signin" { 481 + oauth2Config, err := me.Oauth2Config(r.Host) 482 + if err != nil { 483 + http.Error(w, fmt.Sprintf("failed to get oauth2 config: %v", err), http.StatusInternalServerError) 484 + return 485 + } 486 + 487 + var successURL string 488 + if param := r.URL.Query().Get("success_url"); param != "" { 489 + successURL = fmt.Sprintf("https://%s%s", r.Host, param) 490 + } else if r.Header.Get("Referer") != "" { 491 + successURL = r.Header.Get("Referer") 492 + } else { 493 + successURL = fmt.Sprintf("https://%s/", r.Host) 494 + } 495 + 496 + state := rand.Text() 497 + verifier := oauth2.GenerateVerifier() 498 + authData := AuthData{ 499 + State: state, 500 + SuccessURL: successURL, 501 + CodeVerifier: verifier, 487 502 } 488 503 489 - for _, privateRoute := range k.Strings(fmt.Sprintf("apps.%s.privateRoutes", appname)) { 490 - if isMatch, _ := doublestar.Match(privateRoute, route); isMatch { 491 - routeisProtected = true 492 - break 493 - } 504 + // Marshal the struct to JSON 505 + jsonData, err := json.Marshal(authData) 506 + if err != nil { 507 + http.Error(w, "Server error", http.StatusInternalServerError) 508 + return 494 509 } 495 510 496 - return routeisProtected 511 + encodedData := base64.StdEncoding.EncodeToString(jsonData) 512 + http.SetCookie(w, &http.Cookie{ 513 + Name: "oauth_data", 514 + Value: encodedData, 515 + Secure: true, 516 + HttpOnly: true, 517 + MaxAge: 5 * 60, 518 + SameSite: http.SameSiteLaxMode, 519 + }) 520 + 521 + http.Redirect(w, r, oauth2Config.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier)), http.StatusTemporaryRedirect) 522 + return 497 523 } 498 524 499 - if me.oidcProvider != nil { 500 - r.Header.Del("Remote-Email") 501 - r.Header.Del("Remote-User") 502 - r.Header.Del("Remote-Name") 503 - r.Header.Del("Remote-Groups") 525 + if r.URL.Path == "/_smallweb/signout" { 526 + http.SetCookie(w, &http.Cookie{ 527 + Name: "id_token", 528 + Secure: true, 529 + HttpOnly: true, 530 + MaxAge: -1, 531 + }) 532 + 533 + http.SetCookie(w, &http.Cookie{ 534 + Name: "refresh_token", 535 + Secure: true, 536 + HttpOnly: true, 537 + Path: "/", 538 + MaxAge: -1, 539 + }) 540 + 541 + var successUrl string 542 + if param := r.URL.Query().Get("success_url"); param != "" { 543 + successUrl = fmt.Sprintf("https://%s%s", r.Host, param) 544 + } else if r.Header.Get("Referer") != "" { 545 + successUrl = r.Header.Get("Referer") 546 + } else { 547 + successUrl = fmt.Sprintf("https://%s/", r.Host) 548 + } 549 + 550 + http.Redirect(w, r, successUrl, http.StatusTemporaryRedirect) 551 + return 504 552 } 505 553 506 - if isRouteProtected(r.URL.Path) { 507 - if me.oidcProvider == nil { 508 - http.Error(w, "openauth issuer not set", http.StatusInternalServerError) 554 + if r.URL.Path == "/_smallweb/oauth/callback" { 555 + oauth2Config, err := me.Oauth2Config(r.Host) 556 + if err != nil { 557 + http.Error(w, fmt.Sprintf("failed to get oauth2 config: %v", err), http.StatusInternalServerError) 509 558 return 510 559 } 511 560 512 - clientID := fmt.Sprintf("https://%s", r.Host) 513 - oauth2Config := &oauth2.Config{ 514 - ClientID: clientID, 515 - Scopes: []string{"openid", "email", "profile", "groups"}, 516 - RedirectURL: fmt.Sprintf("https://%s/_smallweb/oauth/callback", r.Host), 517 - Endpoint: me.oidcProvider.Endpoint(), 561 + authCookie, err := r.Cookie("oauth_data") 562 + if err != nil { 563 + http.Error(w, "state cookie not found", http.StatusUnauthorized) 564 + return 518 565 } 519 566 520 - if r.URL.Path == "/_smallweb/signin" { 521 - var successURL string 522 - if param := r.URL.Query().Get("success_url"); param != "" { 523 - successURL = fmt.Sprintf("https://%s%s", r.Host, param) 524 - } else if r.Header.Get("Referer") != "" { 525 - successURL = r.Header.Get("Referer") 526 - } else { 527 - successURL = fmt.Sprintf("https://%s/", r.Host) 528 - } 567 + http.SetCookie(w, &http.Cookie{ 568 + Name: "oauth_data", 569 + Secure: true, 570 + HttpOnly: true, 571 + Path: "/", 572 + MaxAge: -1, 573 + }) 529 574 530 - state := rand.Text() 531 - verifier := oauth2.GenerateVerifier() 532 - authData := AuthData{ 533 - State: state, 534 - SuccessURL: successURL, 535 - CodeVerifier: verifier, 536 - } 575 + decodedData, err := base64.StdEncoding.DecodeString(authCookie.Value) 576 + if err != nil { 577 + http.Error(w, "failed to decode state cookie", http.StatusUnauthorized) 578 + return 579 + } 537 580 538 - // Marshal the struct to JSON 539 - jsonData, err := json.Marshal(authData) 540 - if err != nil { 541 - http.Error(w, "Server error", http.StatusInternalServerError) 542 - return 543 - } 581 + var authData AuthData 582 + if err := json.Unmarshal(decodedData, &authData); err != nil { 583 + http.Error(w, "failed to unmarshal state cookie", http.StatusUnauthorized) 584 + return 585 + } 544 586 545 - encodedData := base64.StdEncoding.EncodeToString(jsonData) 546 - http.SetCookie(w, &http.Cookie{ 547 - Name: "oauth_data", 548 - Value: encodedData, 549 - Secure: true, 550 - HttpOnly: true, 551 - MaxAge: 5 * 60, 552 - SameSite: http.SameSiteLaxMode, 553 - }) 587 + if authData.State != r.URL.Query().Get("state") { 588 + http.Error(w, "invalid state", http.StatusUnauthorized) 589 + return 590 + } 591 + 592 + oauth2Token, err := oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(authData.CodeVerifier)) 593 + if err != nil { 594 + http.Error(w, fmt.Sprintf("failed to exchange code: %v", err), http.StatusInternalServerError) 595 + return 596 + } 554 597 555 - http.Redirect(w, r, oauth2Config.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier)), http.StatusTemporaryRedirect) 598 + idToken := oauth2Token.Extra("id_token").(string) 599 + if idToken == "" { 600 + http.Error(w, "id token not found", http.StatusInternalServerError) 556 601 return 557 602 } 558 603 559 - if r.URL.Path == "/_smallweb/signout" { 560 - http.SetCookie(w, &http.Cookie{ 561 - Name: "id_token", 562 - Secure: true, 563 - HttpOnly: true, 564 - MaxAge: -1, 565 - }) 604 + http.SetCookie(w, &http.Cookie{ 605 + Name: "id_token", 606 + Value: idToken, 607 + SameSite: http.SameSiteLaxMode, 608 + Secure: true, 609 + HttpOnly: true, 610 + Path: "/", 611 + MaxAge: 34560000, 612 + }) 566 613 614 + if oauth2Token.RefreshToken != "" { 567 615 http.SetCookie(w, &http.Cookie{ 568 616 Name: "refresh_token", 617 + Value: oauth2Token.RefreshToken, 618 + SameSite: http.SameSiteLaxMode, 569 619 Secure: true, 570 620 HttpOnly: true, 571 621 Path: "/", 572 - MaxAge: -1, 622 + MaxAge: 34560000, 573 623 }) 574 - 575 - var successUrl string 576 - if param := r.URL.Query().Get("success_url"); param != "" { 577 - successUrl = fmt.Sprintf("https://%s%s", r.Host, param) 578 - } else if r.Header.Get("Referer") != "" { 579 - successUrl = r.Header.Get("Referer") 580 - } else { 581 - successUrl = fmt.Sprintf("https://%s/", r.Host) 582 - } 624 + } 625 + } 583 626 584 - http.Redirect(w, r, successUrl, http.StatusTemporaryRedirect) 627 + email, err := me.extractEmail(r) 628 + if err != nil && IsRoutePrivate(appname, r.URL.Path) { 629 + if !errors.Is(err, &oidc.TokenExpiredError{}) { 630 + http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 585 631 return 586 632 } 587 633 588 - if r.URL.Path == "/_smallweb/oauth/callback" { 589 - authCookie, err := r.Cookie("oauth_data") 634 + var expiredErr *oidc.TokenExpiredError 635 + if errors.As(err, &expiredErr) { 636 + refreshTokenCookie, err := r.Cookie("refresh_token") 590 637 if err != nil { 591 - http.Error(w, "state cookie not found", http.StatusUnauthorized) 638 + http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 592 639 return 593 640 } 594 641 595 - http.SetCookie(w, &http.Cookie{ 596 - Name: "oauth_data", 597 - Secure: true, 598 - HttpOnly: true, 599 - Path: "/", 600 - MaxAge: -1, 601 - }) 602 - 603 - decodedData, err := base64.StdEncoding.DecodeString(authCookie.Value) 642 + oauth2Config, err := me.Oauth2Config(r.Host) 604 643 if err != nil { 605 - http.Error(w, "failed to decode state cookie", http.StatusUnauthorized) 644 + http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 606 645 return 607 646 } 608 647 609 - var authData AuthData 610 - if err := json.Unmarshal(decodedData, &authData); err != nil { 611 - http.Error(w, "failed to unmarshal state cookie", http.StatusUnauthorized) 648 + tokenSource := oauth2Config.TokenSource(context.Background(), &oauth2.Token{RefreshToken: refreshTokenCookie.Value}) 649 + oauth2Token, err := tokenSource.Token() 650 + if err != nil { 651 + http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 612 652 return 613 653 } 614 654 615 - if authData.State != r.URL.Query().Get("state") { 616 - http.Error(w, "invalid state", http.StatusUnauthorized) 655 + rawIdToken, ok := oauth2Token.Extra("id_token").(string) 656 + if !ok { 657 + http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 617 658 return 618 659 } 619 660 620 - oauth2Token, err := oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(authData.CodeVerifier)) 661 + verifier := me.oidcProvider.Verifier(&oidc.Config{ClientID: r.Host}) 662 + idToken, err := verifier.Verify(r.Context(), rawIdToken) 621 663 if err != nil { 622 - http.Error(w, fmt.Sprintf("failed to exchange code: %v", err), http.StatusInternalServerError) 664 + http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 623 665 return 624 666 } 625 667 626 - idToken := oauth2Token.Extra("id_token").(string) 627 - if idToken == "" { 628 - http.Error(w, "id token not found", http.StatusInternalServerError) 668 + var claims struct { 669 + Email string `json:"email"` 670 + } 671 + 672 + if err := idToken.Claims(&claims); err != nil { 673 + http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin", r.Host), http.StatusTemporaryRedirect) 629 674 return 630 675 } 631 676 632 677 http.SetCookie(w, &http.Cookie{ 633 678 Name: "id_token", 634 - Value: idToken, 679 + Value: rawIdToken, 635 680 SameSite: http.SameSiteLaxMode, 636 681 Secure: true, 637 682 HttpOnly: true, ··· 650 695 MaxAge: 34560000, 651 696 }) 652 697 } 653 - 654 - http.Redirect(w, r, authData.SuccessURL, http.StatusTemporaryRedirect) 655 - return 656 698 } 657 - 658 - idTokenCookie, err := r.Cookie("id_token") 659 - if err != nil { 660 - http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin?success_url=%s", r.Host, r.URL.Path), http.StatusTemporaryRedirect) 661 - return 662 - } 663 - 664 - verifier := me.oidcProvider.Verifier(&oidc.Config{ClientID: clientID}) 665 - idToken, err := verifier.Verify(r.Context(), idTokenCookie.Value) 666 - if err != nil { 667 - var expiredErr *oidc.TokenExpiredError 668 - if errors.As(err, &expiredErr) { 669 - refreshTokenCookie, err := r.Cookie("refresh_token") 670 - if err != nil { 671 - http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin?success_url=%s", r.Host, r.URL.Path), http.StatusTemporaryRedirect) 672 - return 673 - } 674 - 675 - tokenSource := oauth2Config.TokenSource(context.Background(), &oauth2.Token{RefreshToken: refreshTokenCookie.Value}) 676 - oauth2Token, err := tokenSource.Token() 677 - if err != nil { 678 - http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin?success_url=%s", r.Host, r.URL.Path), http.StatusTemporaryRedirect) 679 - return 680 - } 681 - 682 - idToken, ok := oauth2Token.Extra("id_token").(string) 683 - if !ok { 684 - http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin?success_url=%s", r.Host, r.URL.Path), http.StatusTemporaryRedirect) 685 - return 686 - } 687 - 688 - http.SetCookie(w, &http.Cookie{ 689 - Name: "id_token", 690 - Value: idToken, 691 - SameSite: http.SameSiteLaxMode, 692 - Secure: true, 693 - HttpOnly: true, 694 - Path: "/", 695 - MaxAge: 34560000, 696 - }) 697 - 698 - if oauth2Token.RefreshToken != "" { 699 - http.SetCookie(w, &http.Cookie{ 700 - Name: "refresh_token", 701 - Value: oauth2Token.RefreshToken, 702 - SameSite: http.SameSiteLaxMode, 703 - Secure: true, 704 - HttpOnly: true, 705 - Path: "/", 706 - MaxAge: 34560000, 707 - }) 708 - } 699 + } 709 700 710 - } else { 711 - http.Redirect(w, r, fmt.Sprintf("https://%s/_smallweb/signin?success_url=%s", r.Host, r.URL.Path), http.StatusTemporaryRedirect) 712 - return 713 - } 714 - } 715 - 716 - // Extract custom claims 717 - var claims struct { 718 - Email string `json:"email"` 719 - Subject string `json:"sub"` 720 - Name string `json:"name"` 721 - Groups []string `json:"groups"` 722 - } 723 - 724 - if err := idToken.Claims(&claims); err != nil { 725 - http.Error(w, fmt.Sprintf("failed to get claims: %v", err), http.StatusInternalServerError) 726 - return 727 - } 728 - 701 + if IsRoutePrivate(appname, r.URL.Path) { 729 702 var authorizedEmails []string 730 703 authorizedEmails = append(authorizedEmails, k.Strings("authorizedEmails")...) 731 704 authorizedEmails = append(authorizedEmails, k.Strings(fmt.Sprintf("apps.%s.authorizedEmails", appname))...) 732 - if len(authorizedEmails) > 0 && !slices.Contains(authorizedEmails, claims.Email) { 705 + if len(authorizedEmails) > 0 && !slices.Contains(authorizedEmails, email) { 733 706 http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 734 707 return 735 708 } 709 + } 736 710 737 - r.Header.Set("Remote-Email", claims.Email) 738 - r.Header.Set("Remote-User", claims.Subject) 739 - r.Header.Set("Remote-Name", claims.Name) 740 - r.Header.Set("Remote-Groups", strings.Join(claims.Groups, ",")) 711 + if email != "" { 712 + r.Header.Set("Remote-Email", email) 741 713 } 742 714 743 715 wk, err := me.GetWorker(appname, k.String("dir"), k.String("domain")) ··· 756 728 wk.ServeHTTP(w, r) 757 729 } 758 730 731 + func IsRoutePrivate(appname string, route string) bool { 732 + isPrivate := k.Bool(fmt.Sprintf("apps.%s.private", appname)) 733 + 734 + for _, publicRoute := range k.Strings(fmt.Sprintf("apps.%s.publicRoutes", appname)) { 735 + if ok, _ := doublestar.Match(publicRoute, route); ok { 736 + isPrivate = false 737 + } 738 + } 739 + 740 + for _, privateRoute := range k.Strings(fmt.Sprintf("apps.%s.privateRoutes", appname)) { 741 + if ok, _ := doublestar.Match(privateRoute, route); ok { 742 + isPrivate = true 743 + } 744 + } 745 + 746 + return isPrivate 747 + } 748 + 749 + func (me *Handler) extractEmail(r *http.Request) (string, error) { 750 + if me.oidcProvider == nil { 751 + return r.Header.Get("Remote-Email"), nil 752 + } 753 + 754 + r.Header.Del("Remote-Email") 755 + idTokenCookie, err := r.Cookie("id_token") 756 + if err != nil { 757 + return "", fmt.Errorf("id token not found") 758 + } 759 + 760 + verifier := me.oidcProvider.Verifier(&oidc.Config{ClientID: fmt.Sprintf("https://%s", r.Host)}) 761 + idToken, err := verifier.Verify(r.Context(), idTokenCookie.Value) 762 + if err != nil { 763 + return "", fmt.Errorf("failed to verify id token: %v", err) 764 + } 765 + 766 + var claims struct { 767 + Email string `json:"email"` 768 + } 769 + 770 + if err := idToken.Claims(&claims); err != nil { 771 + return "", fmt.Errorf("failed to extract claims: %v", err) 772 + } 773 + 774 + return claims.Email, nil 775 + } 776 + 759 777 func lookupApp(domain string) (app string, redirect bool, found bool) { 760 778 if domain == k.String("domain") { 761 779 return "www", true, true ··· 782 800 } 783 801 784 802 return "", false, false 803 + } 804 + 805 + func (me *Handler) Oauth2Config(host string) (*oauth2.Config, error) { 806 + if me.oidcProvider == nil { 807 + return nil, fmt.Errorf("oidc provider not found") 808 + } 809 + 810 + clientID := fmt.Sprintf("https://%s", host) 811 + return &oauth2.Config{ 812 + ClientID: clientID, 813 + Scopes: []string{"openid", "email", "profile", "groups"}, 814 + RedirectURL: fmt.Sprintf("https://%s/_smallweb/oauth/callback", host), 815 + Endpoint: me.oidcProvider.Endpoint(), 816 + }, nil 785 817 } 786 818 787 819 func (me *Handler) GetWorker(appname string, rootDir, domain string) (*worker.Worker, error) {