Skip to content

Secure Authentication

Self provides a secure authentication workflow that allows users to authenticate themselves using their digital identity. This workflow is designed to be scalable and can be used in a variety of scenarios, such as:

  • Secure login for web applications
  • Secure login for mobile applications
  • Secure login for desktop applications

How to build a secure authentication workflow

You can build a simple secure authentication workflow with Self using just a few key features. Let's break down the process into steps:

1. Server: Credential Presentation Request

Once we have the user's address (userAddress), we can request specific credentials from them. In this example, we're requesting a liveness credential. For more details on credential presentation requests, check out the Credential Presentation guide.

content, err = message.NewCredentialPresentationRequest().
    Type([]string{"VerifiablePresentation", "CustomPresentation"}).
    Details(credential.CredentialTypeLiveness, []*message.CredentialPresentationDetailParameter{message.NewCredentialPresentationDetailParameter(message.OperatorNotEquals, "sourceImageHash", "")}).
    Finish()

if err != nil {
    log.Fatal("failed to encode credential request message", "error", err)
}

err = selfAccount.MessageSend(userAddress, content)
if err != nil {
    log.Fatal("failed to send credential request message", "error", err)
}
1
2
3
4
5
6
7
val content = NewCredentialPresentationRequest()
.presentationType(arrayOf("VerifiablePresentation", "CustomPresentation"))
.details(credentialType = arrayOf("VerifiableCredential","LivenessCredential"), parameters = arrayOf(CredentialPresentationDetailParameter.create(operator = ComparisonOperator.NOT_EQUALS, claimField = "sourceImageHash", claimValue = "")))
.finish()

val sendStatus = account.messageSend(userAddress, content)
println("send CredentialPresentation request status: ${sendStatus.code()} - requestId:${credentialRequest.id().toHexString()}")

2. Client: Handle Credential Request

You need to listen to the request in the account’s callback and then check the incoming message for its type and details.
If the request details are correct, open the Liveness UI flow. After receiving the liveness credentials, send the response back.

var credentialRequest: CredentialRequest? = null 
account.setOnRequestListener { msg ->
    when (msg) {
        is CredentialRequest -> {
            if (msg.details().any { it.types().contains(CredentialType.Liveness) && it.subject() == Constants.SUBJECT_SOURCE_IMAGE_HASH }) {                    
                credentialRequest = msg

                navController.navigate("livenessRoute") // launch liveness UI flow
            }
        }
    }
}

// integrate Liveness UI flow into Navigation
addLivenessCheckRoute(navController, route = "livenessRoute", selfModifier = selfModifier, account = { account }, withCredential = true,
    onFinish = { selfie, credentials ->
        if (credentialRequest != null) {
            // after receiving the liveness credentials, send the response
            sendCredentialResponse(credentials)               
        }            
    }
)

fun sendCredentialResponse(credentials: List<Credential>) {       
    val credentialResponse = CredentialResponse.Builder()
        .setRequestId(credentialRequest.id())
        .setTypes(credentialRequest.types())
        .setToIdentifier(credentialRequest.toIdentifier())
        .setFromIdentifier(credentialRequest.fromIdentifier())
        .setStatus(ResponseStatus.accepted)
        .setCredentials(credentials)
        .build()

    account.send(credentialResponse)       
}

See full examples

// 1. Handle credential request in your request listener
account.setOnRequestListener { [weak self] requestMessage in
    guard let self = self else { return }

    if let credentialRequest = requestMessage as? CredentialRequest {
        print("Credential request received from: \(credentialRequest.fromIdentifier())")

        Task { @MainActor in
            await self.handleCredentialRequest(credentialRequest)
        }
    }
}

// Check credential request type to process to right flows
// 2. Handle credential request with proper filtering and thread safety
private func handleCredentialRequest(_ credentialRequest: CredentialRequest) async {
    // Check all requested credentials - filter for liveness credentials
    let allClaims: [Claim] = credentialRequest.details()
    let livenessCredentials = allClaims.filter {
        $0.types().contains(CredentialType.Liveness) &&
        $0.types().contains(CredentialType.Verifiable)
    }
    let otherCredentials = allClaims.filter {
        !($0.types().contains(CredentialType.Liveness) &&
          $0.types().contains(CredentialType.Verifiable))
    }

    print("Total credentials requested: \(allClaims.count)")
    print("Liveness credentials: \(livenessCredentials.count)")
    print("Other credentials: \(otherCredentials.count)")

    // Log other credential types for debugging
    for claim in otherCredentials {
        print("Ignoring unsupported credential types: \(claim.types())")
    }

    if !livenessCredentials.isEmpty {
        print("✅ Processing liveness credentials only")
        await handleLivenessAuthentication(credentialRequest, livenessCredentials: livenessCredentials)
    } else {
        print("❌ No liveness credentials found - this app only supports liveness authentication")
    }
}

// This is an example to show Liveness Authentication
// 3. Handle liveness authentication with proper thread management
private func handleLivenessAuthentication(
    _ credentialRequest: CredentialRequest,
    livenessCredentials: [Claim]
) async {
    // Analyze liveness requirements from filtered credentials
    for claim in livenessCredentials {
        let claimTypes = claim.types()
        let subject = claim.subject()
        let comparisonOperator = claim.comparisonOperator()
        print("Processing liveness claim: types=\(claimTypes), subject=\(subject), operator=\(comparisonOperator)")
    }

    // Present liveness check flow on main thread (critical for UI)
    SelfSDK.showLiveness(
        account: account,
        showIntroduction: false,
        autoDismiss: true
    ) { [weak self] selfieImageData, credentials, error in
        guard let self = self else { return }

        if let error = error {
            print("Liveness check failed: \(error)")
            return
        }

        guard !credentials.isEmpty else {
            print("No credentials provided from liveness check")
            return
        }

        // Send credential response
        Task { @MainActor in
            await self.sendCredentialResponse(
                to: credentialRequest,
                credentials: credentials
            )
        }
    }
}

// Response to the above credential request
// 4. Send credential response with proper type conversion
private func sendCredentialResponse(
    to request: CredentialRequest,
    credentials: [Any]
) async {
    // Convert credentials to proper type (critical for Swift type safety)
    let selfCredentials = credentials.compactMap { $0 as? Credential }

    guard !selfCredentials.isEmpty else {
        print("No valid credentials to send")
        return
    }

    do {
        let response = CredentialResponse.Builder()
            .withRequestId(request.id())
            .withTypes(request.types())
            .toIdentifier(request.toIdentifier())
            .withStatus(ResponseStatus.accepted)
            .withCredentials(selfCredentials)
            .build()

        try await account.send(message: response) { messageId, error in
            if let error = error {
                print("❌ Response failed: \(error)")
            } else {
                print("✅ Credentials sent successfully")
            }
        }
    } catch {
        print("Error building credential response: \(error)")
    }
}

3. Server: Validating Credentials

After receiving the credential presentation response, we need to validate the credentials. This involves checking the presentation's validity, the credential's validity, and extracting the claims. For more information on credential validation, see the Credential Validation guide.

for _, credential := range presentation.Credentials() {
    if credential.CredentialType()[0] == "VerifiableCredential" && credential.CredentialType()[1] == "LivenessCredential" {
        err = credential.Validate()
        if err != nil {
            log.Printf("WARN: failed to validate credential - error: %v", err)
            continue
        }

        if credential.ValidFrom().After(time.Now()) {
            log.Println("WARN: credential is intended to be used in the future")
            continue
        }

        claims, err := credential.CredentialSubjectClaims()
        if err != nil {
            log.Printf("WARN: failed to parse credential claims - error: %v", err)
            continue
        }

        for k, v := range claims {
            log.Printf("INFO: credential value - credentialType: %s, field: %s, value: %v", credential.CredentialType(), k, v)
        }
    }
}
presentation.credentials().forEach { credential ->
    credential.validate()

    val claims = credential.credentialSubjectClaims()

    claims.forEach {
        println(
            "credential value" +
            "\ncredentialType:${credential.credentialType()}" +
            "\nfield:${it.key}" +
            "\nvalue:${it.value}"
        )
    }
}

Conclusion

This example demonstrates how to build a simple secure authentication workflow with Self. We've seen how to use Self Discovery to find the DIDs of the users we want to authenticate, how to send a credential presentation request for the appropriate credentials, and how to validate the presentations and credentials.

For the complete example, including error handling and additional details, please refer to our GitHub repository.

By leveraging these Self features, you can create a robust, secure, and privacy-preserving authentication system for your applications.