code.gitea.io/gitea@v1.21.7/routers/web/auth/webauthn.go (about)

     1  // Copyright 2018 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package auth
     5  
     6  import (
     7  	"errors"
     8  	"net/http"
     9  
    10  	"code.gitea.io/gitea/models/auth"
    11  	user_model "code.gitea.io/gitea/models/user"
    12  	wa "code.gitea.io/gitea/modules/auth/webauthn"
    13  	"code.gitea.io/gitea/modules/base"
    14  	"code.gitea.io/gitea/modules/context"
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/setting"
    17  	"code.gitea.io/gitea/services/externalaccount"
    18  
    19  	"github.com/go-webauthn/webauthn/protocol"
    20  	"github.com/go-webauthn/webauthn/webauthn"
    21  )
    22  
    23  var tplWebAuthn base.TplName = "user/auth/webauthn"
    24  
    25  // WebAuthn shows the WebAuthn login page
    26  func WebAuthn(ctx *context.Context) {
    27  	ctx.Data["Title"] = ctx.Tr("twofa")
    28  
    29  	// Check auto-login.
    30  	if checkAutoLogin(ctx) {
    31  		return
    32  	}
    33  
    34  	// Ensure user is in a 2FA session.
    35  	if ctx.Session.Get("twofaUid") == nil {
    36  		ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
    37  		return
    38  	}
    39  
    40  	hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Session.Get("twofaUid").(int64))
    41  	if err != nil {
    42  		ctx.ServerError("HasTwoFactorByUID", err)
    43  		return
    44  	}
    45  
    46  	ctx.Data["HasTwoFactor"] = hasTwoFactor
    47  
    48  	ctx.HTML(http.StatusOK, tplWebAuthn)
    49  }
    50  
    51  // WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
    52  func WebAuthnLoginAssertion(ctx *context.Context) {
    53  	// Ensure user is in a WebAuthn session.
    54  	idSess, ok := ctx.Session.Get("twofaUid").(int64)
    55  	if !ok || idSess == 0 {
    56  		ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
    57  		return
    58  	}
    59  
    60  	user, err := user_model.GetUserByID(ctx, idSess)
    61  	if err != nil {
    62  		ctx.ServerError("UserSignIn", err)
    63  		return
    64  	}
    65  
    66  	exists, err := auth.ExistsWebAuthnCredentialsForUID(ctx, user.ID)
    67  	if err != nil {
    68  		ctx.ServerError("UserSignIn", err)
    69  		return
    70  	}
    71  	if !exists {
    72  		ctx.ServerError("UserSignIn", errors.New("no device registered"))
    73  		return
    74  	}
    75  
    76  	assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user))
    77  	if err != nil {
    78  		ctx.ServerError("webauthn.BeginLogin", err)
    79  		return
    80  	}
    81  
    82  	if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil {
    83  		ctx.ServerError("Session.Set", err)
    84  		return
    85  	}
    86  	ctx.JSON(http.StatusOK, assertion)
    87  }
    88  
    89  // WebAuthnLoginAssertionPost validates the signature and logs the user in
    90  func WebAuthnLoginAssertionPost(ctx *context.Context) {
    91  	idSess, ok := ctx.Session.Get("twofaUid").(int64)
    92  	sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData)
    93  	if !ok || !okData || sessionData == nil || idSess == 0 {
    94  		ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
    95  		return
    96  	}
    97  	defer func() {
    98  		_ = ctx.Session.Delete("webauthnAssertion")
    99  	}()
   100  
   101  	// Load the user from the db
   102  	user, err := user_model.GetUserByID(ctx, idSess)
   103  	if err != nil {
   104  		ctx.ServerError("UserSignIn", err)
   105  		return
   106  	}
   107  
   108  	log.Trace("Finishing webauthn authentication with user: %s", user.Name)
   109  
   110  	// Now we do the equivalent of webauthn.FinishLogin using a combination of our session data
   111  	// (from webauthnAssertion) and verify the provided request.0
   112  	parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
   113  	if err != nil {
   114  		// Failed authentication attempt.
   115  		log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
   116  		ctx.Status(http.StatusForbidden)
   117  		return
   118  	}
   119  
   120  	// Validate the parsed response.
   121  	cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse)
   122  	if err != nil {
   123  		// Failed authentication attempt.
   124  		log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
   125  		ctx.Status(http.StatusForbidden)
   126  		return
   127  	}
   128  
   129  	// Ensure that the credential wasn't cloned by checking if CloneWarning is set.
   130  	// (This is set if the sign counter is less than the one we have stored.)
   131  	if cred.Authenticator.CloneWarning {
   132  		log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr())
   133  		ctx.Status(http.StatusForbidden)
   134  		return
   135  	}
   136  
   137  	// Success! Get the credential and update the sign count with the new value we received.
   138  	dbCred, err := auth.GetWebAuthnCredentialByCredID(ctx, user.ID, cred.ID)
   139  	if err != nil {
   140  		ctx.ServerError("GetWebAuthnCredentialByCredID", err)
   141  		return
   142  	}
   143  
   144  	dbCred.SignCount = cred.Authenticator.SignCount
   145  	if err := dbCred.UpdateSignCount(ctx); err != nil {
   146  		ctx.ServerError("UpdateSignCount", err)
   147  		return
   148  	}
   149  
   150  	// Now handle account linking if that's requested
   151  	if ctx.Session.Get("linkAccount") != nil {
   152  		if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil {
   153  			ctx.ServerError("LinkAccountFromStore", err)
   154  			return
   155  		}
   156  	}
   157  
   158  	remember := ctx.Session.Get("twofaRemember").(bool)
   159  	redirect := handleSignInFull(ctx, user, remember, false)
   160  	if redirect == "" {
   161  		redirect = setting.AppSubURL + "/"
   162  	}
   163  	_ = ctx.Session.Delete("twofaUid")
   164  
   165  	ctx.JSONRedirect(redirect)
   166  }