Password를 대체하는 기술
Password의 단점
iOS 15에서 PassKey를 개발자 베타로 오픈했고, iOS 16에서 정식으로 출시한다.
유저가 사용하기 편할 뿐 아니라, 모든 영역의 보안 문제를 개선한다.
PassKey를 생성하고 나면, 사용법은 autofill과 비슷하다.
앱 뿐 아니라 앱에서도 사용 가능하다.
FIDO에서 여러 회사와 협업해서 만든 오픈 스탠다드를 기반으로 한다.
이 QR을 스캔하면 뒤에서는 이런 일들이 일어난다.
이 PassKey는 airdrop을 통해서 타인과 공유할 수 있다.
Designing for passkeys
Passkeys and Autofill
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"
그 외에도 웹에서 고려할 사항
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
Multi-factor authentication
패스워드 없는 세상을 위해서는