• Password를 대체하는 기술

  • Password의 단점

    • 올바르게 쓰기 힘들다(몇자 이상, 대소문자, 특수문자, 숫자 포함…)
    • 보안을 챙기려고 하면 복잡해진다.
    • 피싱될 수 있고, 재사용 될 수 있다.
  • iOS 15에서 PassKey를 개발자 베타로 오픈했고, iOS 16에서 정식으로 출시한다.

  • 유저가 사용하기 편할 뿐 아니라, 모든 영역의 보안 문제를 개선한다.

    • 취약하고 재사용되는 credential 문제 해결
    • credential유출 문제 해결
  • PassKey를 생성하고 나면, 사용법은 autofill과 비슷하다.

    • 차이점은 시스템에 의해서 만들어지고, 계정마다 다른 PassKey가 만들어 진다는 것이다.
  • 웹 뿐 아니라 앱에서도 사용 가능하다.

  • FIDO에서 여러 회사와 협업해서 만든 오픈 스탠다드를 기반으로 한다.

    • 그렇기에 애플 디바이스가 아닌 경우에도 쓸 수 있다
    • 로컬에 Passkey가 없는 경우, QR이 뜨고 이를 PassKey가 있는 모바일 디바이스로 스캔하면 로그인이 된다.
  • 이 QR을 스캔하면 뒤에서는 이런 일들이 일어난다.

    • local key agreement
    • providing proximity(근접 상태임을 확인)
    • establishing and e2e encrypted communication channel
  • 이 PassKey는 airdrop을 통해서 타인과 공유할 수 있다.

    • 사람이 칠 수 있는 형태는 아니기 때문에 이런 식으로 공유하게 된다.
  • Designing for passkeys

    • passkey는 일반 명사고 유저에게 보여질 수 있는 단어다.
      • 영상에는 애플 쪽 구현만 이야기하지만, 각 플랫폼에서 이미 구현을 시작했다.
      • password처럼 소문자로 쓰고 복수형으로 쓸 때도 그냥 s만 붙이면 된다.
    • SF Symbol에서는 person.key.badge(.fil) 로 나타내고 있다.
    • 기존 로그인 화면을 리디자인 할 필요없이, Password만 지워지고, username에서 연관된 passkey를 보여주게 된다.
      • 한 사이트에서 여러개의 username을 쓰는 것도 대응이 되어있다.
  • Passkeys and Autofill

    • WebAuth 기반
      • 공개키 암호화
      • 서버에서 WebAuth 구현을 해줘야 함 → 표준 WebAuth를 구현했다면 반드시 Passkey가 동작해야 함
    • 애플 쪽 구현
      • ASAuthorization 계열 → Password, 보안키, sign in with apple에서 쓰던 것

      • autofill지원 등의 추가 API를 지원해서, 유연하고 기존 흐름에도 잘 맞을 수 있도록 함

      • 서버쪽에 associated-domain을 설정해야 함

        // <https://example.com//.well-known/apple-app-site-association>
        {
            "webcredentials": {
                "apps": [ "A1B2C3D4E5.com.example.Shiny" ]
            }
        }
        
      • username을 받는 textfield는 .username 컨텐츠 타입을 설정해야 한다.

        override func viewDidLoad() {
            super.viewDidLoad()
            //Additional setup…
        
            userNameField.textContentType = .username
        }
        
        • Passkey 요청 예제

          // AutoFill-assisted passkey request
          
          func signIn() {
          		// challenge를 서버에서 받아온다.
              let challenge: Data = … 
          
          		// provider와 request 설정
              let provider =
                  ASAuthorizationPlatformPublicKeyCredentialProvider(
                      relyingPartyIdentifier: "example.com")
              let request =
                  provider.createCredentialAssertionRequest(
                      challenge: challenge)
          		
          		// 실제 요청을 핸들링하는 controller 생성
              let controller =
                  ASAuthorizationController(
                      authorizationRequests: [request])
              controller.delegate = self
              controller.presentationContextProvider = self
          
              // request 실행. username 텍스트 field가 focus되기 전에 호출이 완료 되어야 함
              controller.performAutoFillAssistedRequests()
          }
          
        • 성공시 콜백

          // Completing a passkey sign in
          
          func authorizationController(controller: ASAuthorizationController,
               didCompleteWithAuthorization authorization: ASAuthorization) {
              
              guard let passkeyAssertion = authorization.credential as?
                  ASAuthorizationPlatformPublicKeyCredentialAssertion
              else { … }
          
          		// 백엔드에 보낼 값을 꺼낸다.
              let signature = passkeyAssertion.signature
              let clientDataJSON = passkeyAssertion.rawClientDataJSON
          
              // Pass these values to your server, and complete the sign in
          …
          }
          
      • 다른 디바이스를 통해서 로그인도 가능(코드 변경 없이)

      • Passkey가 없는 경우를 대비해 password등의 fallback은 필요.

      • autofill을 쓰면 username입력을 안해도 되지만, 그래도 username을 입력한 사람은 autofill request 대신 modal을 띄워서 passkey 로그인을 할 수 있다.

        // Modal passkey request
        
        func signIn() {
            let challenge: Data = … // Fetched from server
            let provider =      
                ASAuthorizationPlatformPublicKeyCredentialProvider(
                    relyingPartyIdentifier: "example.com")
            let request = 
                provider.createCredentialAssertionRequest(
                    challenge: challenge)
            
            let controller = 
                ASAuthorizationController(
                    authorizationRequests: [request])
            controller.delegate = self
            controller.presentationContextProvider = self
        
            // Start the request
            controller.performRequests()
        }
        
      • 웹에서도 똑같이 지원한다.

        <input type="text" id="username-field" autocomplete="username webauthn" >
        
        // AutoFill-assisted WebAuthn request (JavaScript)
        
        function signIn() {
            if (!PublicKeyCredential.isConditionalMediationAvailable ||
                !PublicKeyCredential.isConditionalMediationAvailable()) {
                // Browser doesn't support AutoFill-assisted requests.
                return;
            }
        
            const options = {
                "publicKey": {
                    challenge: … // Fetched from server
                },
                mediation: "conditional" // autofill 지원
            };
        
            navigator.credentials.get(options)
                .then(assertion => { 
                    // Pass the assertion to your server.
                });
        }
        
      • 이 때 UserVerification 핸들링에 신경 쓸 필요가 있다.

        • 이는 WebAuth 응답에 딸려오는 Boolean값으로, 현재 유저가 이 디바이스의 주인인지를 검증한 결과이다.

        • 애플 플랫폼에서는 생체 인증 혹은 패스코드를 사용했다는 뜻이다.

          • 애플 플랫폼에서는 생체 인증이 필요하면 무조건 요구하도록 되어 있다.
        • request를 만들 때 user verification 필요 여부를 설정할 수 있는데, 항상 기본값을 써라

          • 이걸 강제하면 생체 인증이 없는 기기에서의 경험이 좋지 않기 때문이다.
          userVerification: "preferred"
          
      • 그 외에도 웹에서 고려할 사항

        • autofill 요청은 일찍 하라 → 앱에서도 포커스 가기 전이 미리 해놔야 했던 것
        • modal request는 유저 제스처를 통해서 하라.(버튼 클릭이라던지)
          • 유저 제스처가 아닌 상황에서도 요청을 할 수는 있는데, 그렇게하면 웹킷이 다음부터는 호출 못하게 막아버린다.
        • Passkey는 기존의 Safari의 인증기를 대체한다.
          • 기존에 있던 credential은 여전히 동작하고, 기기에도 남아있겠지만 새로 만드는 것들은 모두 Passkey로 만들어 질 거다.
  • Streamlining sign-in

    • Using passkey allow lists

      • modal요청을 할 때, 기본적으로 기기에 있는 모든 연관된 Passkey를 보여주게 된다.

      • 이를 필터링 할 수 있는 기능이 있다.

        // Modal request with allow list
        
        func signIn(userName: String) {
            let challenge: Data = … // Fetched from server
            let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
                relyingPartyIdentifier:"example.com")
            let request = provider.createCredentialAssertionRequest(
                challenge: challenge)
        
            let credentialIDs: [Data] = … // Fetched from server for provided userName
            request.allowedCredentials = credentialIDs.map(
                ASAuthorizationPlatformPublicKeyCredentialDescriptor.init(credentialID:))
        
            let controller = ASAuthorizationController(authorizationRequests: [request])
            controller.delegate = self
            controller.presentationContextProvider = self
        
            // Start the request
            controller.performRequests()
        }
        
    • Silent fallback requests

      • 기기에 Passkey가 없는 경우는 자동으로 nearby device 로그인(QR 띄우기)를 보여준다.

      • 하지만 요청할 때 옵션을 주면 바로 delegate에서 에러가 전달되게 하고, 이를 기반으로 기존 로그인 방법으로 fallback할 수 있다.

        // Modal passkey request, silent fallback
        
        func signIn() {
            let challenge: Data = … // Fetched from server
            let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
                relyingPartyIdentifier:"example.com")
            let request = provider.createCredentialAssertionRequest(
                challenge: challenge)
        
            let controller = ASAuthorizationController(authorizationRequests: [request])
            controller.delegate = self
            controller.presentationContextProvider = self
        
            // Start the request
            controller.performRequests(options: .preferImmediatelyAvailableCredentials)
        }
        
        // delegate
        
        // Handling a silent fallback
            
        func authorizationController(controller: ASAuthorizationController, 
            didCompleteWithError error: Error) {
            
            guard let error = error as? ASAuthorizationError else { … }
        
            if error.code == .canceled {
                // Either the user canceled the sheet, or there were no credentials available.
                showSignInForm()
            }
        }
        
    • Combined credential requests

      • 만약 이미 여러가지 로그인 방법을 로컬에 가지고 있는 상태라면?(패스워드, sign in apple 등)

      • 동시에 다 띄워줄 수 있고, 없는 것은 자연스럽게 생략된다.

        // Combined credential modal request
        
        func signIn() {
            let challenge: Data = … // Fetched from server
            let passkeyProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
                relyingPartyIdentifier:"example.com")
            let passkeyRequest = passkeyProvider.createCredentialAssertionRequest(
                challenge: challenge)
        
            let passwordRequest = ASAuthorizationPasswordProvider().createRequest()
            let signInWithAppleRequest = ASAuthorizationAppleIDProvider().createRequest()
        
            let controller = ASAuthorizationController(
                authorizationRequests: [passkeyRequest, passwordRequest, signInWithAppleRequest])
            controller.delegate = self
            controller.presentationContextProvider = self
        
            // Start the request
            controller.performRequests()
        }
        
        // Completing a combined credential request
        
        func authorizationController(controller: ASAuthorizationController, 
             didCompleteWithAuthorization authorization: ASAuthorization) {
        
            switch authorization.credential {
            case let passkeyAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
                finishSignIn(with: passkeyAssertion)
        
            case let signInWithAppleCredential as ASAuthorizationAppleIDCredential:
                finishSignIn(with: signInWithAppleCredential)
        
            case let passwordCredential as ASPasswordCredential:
                finishSignIn(with: passwordCredential)
        
            default:
                // Handle other credential types
                break
            }
        }
        
  • How passkeys work

    • 기존 패스워드의 동작
      • 가입시 패스워드를 입력하면 이를 salted hash해서 서버로 전송한다.
      • 로그인 할 때도 이와 동일한 해쉬값을 만들어서 보낼 수 있으면 로그인을 시켜준다.
        • 이는 서버가 해쉬값을 가지고 있어야 한다는 뜻이다.
    • passkey의 동작
      • 공개키와 비밀키 쌍이 개인 디바이스에 의해서 만들어짐(계정 별로)
      • 공개키는 서버로 가고, 비밀키는 개인 디바이스에만 존재
      • 로그인을 시도할 때, 서버에서는 challenge를 보낸다.
        • WebAuth 표준에서는 다양한 알고리즘을 허용하지만, 애플 플랫폼에서는 표준 ES256을 쓴다.
      • 이 challenge는 private key가 있어야만 올바른 답을 낼 수 있다. 개인 디바이스에서는 이 답을 서버로 전송한다.
      • 서버는 공개키를 이용해서 답을 검증한다.
    • nearby device 로그인 원리
  • Multi-factor authentication

  • 패스워드 없는 세상을 위해서는