-
-
Notifications
You must be signed in to change notification settings - Fork 964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: code review for code
strategy (magic code login)
#3456
Changes from 9 commits
d17bf75
569371f
56dd054
aa2703e
ed4b552
d2dab02
193d33c
b8cdb26
bcd626b
0582f6c
55fa3eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,6 +72,21 @@ func TestSchemaExtensionCredentials(t *testing.T) { | |
}, | ||
ct: identity.CredentialsTypeWebAuthn, | ||
}, | ||
{ | ||
doc: `{"email":"[email protected]"}`, | ||
schema: "file://./stub/extension/credentials/code.schema.json", | ||
expect: []string{"[email protected]"}, | ||
ct: identity.CredentialsTypeCodeAuth, | ||
}, | ||
{ | ||
doc: `{"email":"[email protected]"}`, | ||
schema: "file://./stub/extension/credentials/code.schema.json", | ||
expect: []string{"[email protected]"}, | ||
existing: &identity.Credentials{ | ||
Identifiers: []string{"[email protected]"}, | ||
}, | ||
ct: identity.CredentialsTypeCodeAuth, | ||
}, | ||
} { | ||
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { | ||
c := jsonschema.NewCompiler() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"type": "object", | ||
"properties": { | ||
"email": { | ||
"type": "string", | ||
"format": "email", | ||
"ory.sh/kratos": { | ||
"credentials": { | ||
"password": { | ||
"identifier": true | ||
}, | ||
"code": { | ||
"identifier": true, | ||
"via": "email" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
// Copyright © 2023 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package sql | ||
|
||
import ( | ||
"context" | ||
"crypto/subtle" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/gobuffalo/pop/v6" | ||
"github.com/gofrs/uuid" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/ory/kratos/selfservice/strategy/code" | ||
"github.com/ory/x/sqlcon" | ||
) | ||
|
||
type oneTimeCodeProvider interface { | ||
GetID() uuid.UUID | ||
Validate() error | ||
TableName(ctx context.Context) string | ||
GetHMACCode() string | ||
} | ||
|
||
type codeOptions struct { | ||
IdentityID *uuid.UUID | ||
} | ||
|
||
type codeOption func(o *codeOptions) | ||
|
||
func withCheckIdentityID(id uuid.UUID) codeOption { | ||
return func(o *codeOptions) { | ||
o.IdentityID = &id | ||
} | ||
} | ||
|
||
func useOneTimeCode[P any, U interface { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code was the same for login, verification, recovery, and sign up, so I moved it into a common function. Uses some generic magic to make it work nicely. Tried it with duck typing only at first, but it caused issues and would have required reflection. |
||
*P | ||
oneTimeCodeProvider | ||
}](ctx context.Context, p *Persister, flowID uuid.UUID, userProvidedCode string, flowTableName string, foreignKeyName string, opts ...codeOption) (U, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, Generics can not be avoided because we need to work with and modify the concrete underlying data type, and interfaces have certain limitations such as dealing with uninitialized values. |
||
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.useOneTimeCode") | ||
defer span.End() | ||
|
||
o := new(codeOptions) | ||
for _, opt := range opts { | ||
opt(o) | ||
} | ||
|
||
var target U | ||
nid := p.NetworkID(ctx) | ||
if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { | ||
//#nosec G201 -- TableName is static | ||
if err := tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), flowID, nid).Exec(); err != nil { | ||
return err | ||
} | ||
|
||
var submitCount int | ||
// Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. | ||
//#nosec G201 -- TableName is static | ||
if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), flowID, nid).First(&submitCount)); err != nil { | ||
if errors.Is(err, sqlcon.ErrNoRows) { | ||
// Return no error, as that would roll back the transaction | ||
return nil | ||
} | ||
return err | ||
} | ||
|
||
// This check prevents parallel brute force attacks by checking the submit count inside this database | ||
// transaction. If the flow has been submitted more than 5 times, the transaction is aborted (regardless of | ||
// whether the code was correct or not) and we thus give no indication whether the supplied code was correct or | ||
// not. For more explanation see [this comment](https://github.com/ory/kratos/pull/2645#discussion_r984732899). | ||
if submitCount > 5 { | ||
return errors.WithStack(code.ErrCodeSubmittedTooOften) | ||
} | ||
|
||
var codes []U | ||
codesQuery := tx.Where(fmt.Sprintf("nid = ? AND %s = ?", foreignKeyName), nid, flowID) | ||
if o.IdentityID != nil { | ||
codesQuery = codesQuery.Where("identity_id = ?", *o.IdentityID) | ||
} | ||
|
||
if err := sqlcon.HandleError(codesQuery.All(&codes)); err != nil { | ||
if errors.Is(err, sqlcon.ErrNoRows) { | ||
// Return no error, as that would roll back the transaction and reset the submit count. | ||
return nil | ||
} | ||
|
||
return err | ||
} | ||
|
||
secrets: | ||
for _, secret := range p.r.Config().SecretsSession(ctx) { | ||
suppliedCode := []byte(p.hmacValueWithSecret(ctx, userProvidedCode, secret)) | ||
for i := range codes { | ||
c := codes[i] | ||
if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 { | ||
// Not the supplied code | ||
continue | ||
} | ||
target = c | ||
break secrets | ||
} | ||
} | ||
|
||
if target.Validate() != nil { | ||
// Return no error, as that would roll back the transaction | ||
return nil | ||
} | ||
|
||
//#nosec G201 -- TableName is static | ||
return tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", target.TableName(ctx)), time.Now().UTC(), target.GetID(), nid).Exec() | ||
}); err != nil { | ||
return nil, sqlcon.HandleError(err) | ||
} | ||
|
||
if err := target.Validate(); err != nil { | ||
return nil, err | ||
} | ||
|
||
return target, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These did not have tests