Skip to content

support digest auth for caldav sources - #36

Open
Drizztfire wants to merge 1 commit into
Opisek:mainfrom
Drizztfire:caldav-digest-auth
Open

support digest auth for caldav sources#36
Drizztfire wants to merge 1 commit into
Opisek:mainfrom
Drizztfire:caldav-digest-auth

Conversation

@Drizztfire

Copy link
Copy Markdown

Summary

  • retry username/password CalDAV requests with HTTP Digest auth when the server challenges with WWW-Authenticate: Digest
  • keep the existing Basic auth flow for servers that already accept it
  • allow Luna to connect to Baikal and other Digest-only CalDAV servers without changing the UI auth model

@Opisek

Opisek commented Apr 5, 2026

Copy link
Copy Markdown
Owner

Thank you for the pull request!

Please note that I might take a while to review it, due to being busy with some other things at this moment.

I'll read into digest authentication, test your feature, and get back to you when I find the time.

@Opisek

Opisek commented Apr 5, 2026

Copy link
Copy Markdown
Owner

One thing I can already say is that I would like this to be implemented as a separate authentication method, rather than being merged with basic authentication.

From the UI side of things, let's just add it to the advanced source creation modal first. I can add it to the wizard later.

Due to the way that the currently proposed implementation was built "on top" of basic authentication, you essentially double the amount of requests you need to make if you use digest authentication. A little more is required to overcome this than just splitting it into a separate authentication type.

First, the digest-specific values Realm, Nonce, Opaque, Qop, Algorithm, and Stale should be added directly to the DigestAuth struct (once it's created). These should not be exposed in API responses, so json:"-" should be set. To make sure that we don't need an extra request for every original request, these values should be updated in the database whenever we receive a new digest challenge. Similarly, we need to keep track of the amount of requests done with the received nonce (nc). See how OauthAuthentication handles its database calls.

Edit: I think that creating a new database table for the cached challenges are the way to go. Interacting with the database could be done similarly to what my OAuth 2.0 implementation does, but some more careful considerations might be needed regarding race conditions. Let's worry about that once we get there.

@Opisek Opisek left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feature! There are a few things that I'd like sorted before merging. Refer to the review comments and the comments in the PR's main thread.

return "Digest " + strings.Join(parts, ", "), nil
}

func (auth BasicAuth) doDigest(req *http.Request, challenge *digestChallenge) (*http.Response, *errors.ErrorTrace) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split it into its own authentication type.
-> func (auth DigestAuth) Do(...)

Password string `json:"password" form:"password"`
}

type digestChallenge struct {

@Opisek Opisek Apr 5, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split this into its own authentication type
-> type DigestAuth struct { ... }

Make sure to use json:"-" on the challenge-specific fields, because that is not something we should return to the user.

Edit: Later I made a comment about creating a new database table for this instead of saving the challenges directly within the source. For guidance with that, see how OAuth 2.0 interacts with the database.

ha2 := md5Hex(fmt.Sprintf("%s:%s", req.Method, uri))

response := ""
parts := []string{

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are all later joined to a single string anyway, please use a string builder instead.

}

if qop != "" {
nonceCount := "00000001"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a failure for every single request that we do, since we only ever use each received nonce once.

Instead, we should save the digest challenge and the nc in the database, so we can reuse it across multiple requests. I think it makes the most sense to make a new database table for it.


func newCnonce() (string, error) {
bytes := make([]byte, 16)
_, err := rand.Read(bytes)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use GenerateRandomBytes from luna/crypto.

return challenge
}

func cloneRequest(req *http.Request) (*http.Request, error) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delay cloning the actual request until after we get the challenge (if we get one at all).

For the body, we can io.ReadAll(req.Body) at the beginning of Do, set the original request's body to the read content again, and keep it in the memory in case we need to create a request copy later after all.

Doing it that way should reduce the memory overhead and prevent the "request body is not replayable" error.

key = strings.ToLower(strings.TrimSpace(key))
value = strings.Trim(strings.TrimSpace(value), `"`)

switch key {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a default case and error out if we receive something malformed..

return parts
}

func parseDigestChallenge(header string) *digestChallenge {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return an error, too

return res, nil
}

func (auth BasicAuth) Do(req *http.Request) (*http.Response, *errors.ErrorTrace) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As already mentioned, let's implement digest as its own authentication type and revert basic authentication to how it was.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants