04 Auth
4.7 Application-level Authentication
System-level auth provides very nice inherent benefits. So what role (no pun intended) does application-level auth serve? Application-level auth is the use of custom validation methods within the blueprint to authenticate proofs. It provides more flexibility where there are auth cases the Radix Engine can’t satisfy. Most often, it is generally the use of some
NonFungibleData as an auth measure that is validated by the component. This can be done by manually popping proofs of non-fungibles from the AuthZone and specifying proofs to be directly sent to component method calls within the transaction manifest. This section will cover different ways we can authenticate proofs directly at the application-level and explore how to utilize NonFungibleData of the proofs passed into our methods.//Authorizing an Action with Badges
Throughout this section, we will be using XRDDistributor blueprint as an example to help understand how application-level auth may be useful over system-level auth. The XRDDistributor is a blueprint where when its component is instantiated will provide methods to give away some non-fungible resource to be used as a badge to claim XRD prizes. To limit how much XRD we want to give away, the non-fungible resource will have a
We are also operating under the assumption that the distribution of the “ClaimData” will be controlled as to make sure that no one can simply mint a multitude of these NFTs to claim more XRD than they are allowed to. We will show small snippets of the codebase to highlight areas we want discuss; at the end of the example, we'll show the full implementation of the codebase.
NonFungibleData called "ClaimData". This NonFungibleData has a field called xrd_claimed where it is a boolean that indicates true or false if this non-fungible resource has been used to claim XRD. This way we can manage the frequency of XRD we distribute by only allowing claims of XRD to be redeemable by limiting it to one non-fungible resource at a time. We are also operating under the assumption that the distribution of the “ClaimData” will be controlled as to make sure that no one can simply mint a multitude of these NFTs to claim more XRD than they are allowed to. We will show small snippets of the codebase to highlight areas we want discuss; at the end of the example, we'll show the full implementation of the codebase.
A Brief Look at the Badges
As mentioned, in our XRDDistributor, we will be distributing XRD. For users to claim XRD, they will need to mint an NFT, which we’ll call claim_badge, to use to qualify for XRD giveaway. We’ll also create a second badge called the
minter_badge which the component will hold in its vault to provide the authority to mint claim_badge for users.let minter_badge = ResourceBuilder::new_fungible(OwnerRole::None) .mint_initial_supply(1);let claim_badge = ResourceBuilder::new_ruid_non_fungible::<ClaimData>(OwnerRole::None) .mint_roles(mint_roles!( minter => rule!(require(minter_badge.resource_address())); minter_updater => rule!(deny_all); )) .non_fungible_data_update_roles(non_fungible_data_update_roles!( non_fungible_data_updater => rule!(require(minter_badge.resource_address())); non_fungible_data_updater_updater => rule!(deny_all); )) .create_with_no_initial_supply();Line 1-2: Since the
minter_badge doesn’t need to be anything special other than to give the component authority to mint claim_badge, we’ll simply create a fungible resource for it .Line 4-14: The claim_badge will be a non-fungible resource. This is because the claim_badge will have some NonFungibleData to limit how much XRD can be claimed per claim_badge.Using NonFungibleData for Auth
We will have a
NonFungibleData represented by the ClaimData struct to provide us information whether the claim_badge has been used to claim XRD or not. This will be a field called xrd_claimed and has a boolean value which will first be false when a claim_badge is first minted and updated to true when it is used to claim XRD. As a result, we also need to apply the #[mutable] macro attribute to indicate that this field can be updatable.#[derive(ScryptoSbor, NonFungibleData)]struct ClaimData { #[mutable] xrd_claimed: bool}Passing Proofs by Intent
So now that we have our badges setup, we’ll create a method called
claim_xrd, where users need to call and pass in their claim_badge to claim XRD. This method will have two custom validation:- Check that the
claim_badgeis a resource that belongs to the component and not some other resource that is used to imitate aclaim_badge. - Check the
NonFungibleDataof theclaim_badgeto make sure that thexrd_claimedfield is not set to true indicating that they’ve already used thisclaim_badgeto claim XRD and can no longer qualify for the giveaway using this badge.
pub fn claim_tokens(&mut self, claim_badge_proof: NonFungibleProof) -> FungibleBucket { let checked_proof = claim_badge_proof.check_with_message( self.claim_badge_manager.address(), "Incorrect proof!" ); let nft = checked_proof.non_fungible::<ClaimData>(); let nft_data = nft.data(); // Asserting that the claimed field does not have a value of true. assert_ne!( nft_data.claimed, true, "You have already claimed your tokens" ); self.minter_badge_vault.authorize_with_amount(1, || { self.claim_badge_manager.update_non_fungible_data( &nft.local_id(), "xrd_claimed", true ) }); return self.vault.take(10);}Line 3-6: We retrieve the
Line 17-25: Once our checks has passed, we can be confident that the badge has not yet been used to claim XRD from our component. Therefore we will update the
Here we can see how application-level auth works. Notice that unlike system-level auth, we didn’t require to use
claim_badge_proof and first validate the proof by using .check(). The .check() method takes in a ResourceAddress where you can input the expected resource as an argument. This will compare the ResourceAddress of the badge passed in and the expected ResourceAddress. Line 12-16: At this point of the code, we can be confident that the proof passed in is the proof of the claim_badge. In which case after we retrieve the NonFungibleData, we will write a custom assertion. This assertion will ensure that the value of xrd_claimed is not true (otherwise indicating that this badge has already been used to claim XRD).Line 17-25: Once our checks has passed, we can be confident that the badge has not yet been used to claim XRD from our component. Therefore we will update the
NonFungibleData and then return to the caller the XRD qualified.Here we can see how application-level auth works. Notice that unlike system-level auth, we didn’t require to use
enable_method_auth! macro. Instead, we created a method that required callers to send the proof of their badge into the component. Once this proof is sent to the component there are custom logic we wrote to perform some validation to check not only the proof of the badge being valid but also the value of the NonFungibleData of the proof is what we expect it to be.Dangling resource? We mention many times that resources need to be in a resource container. Whether it’s a bucket or a vault, but we just passed in a proof and it’s not accounted for, what gives?! Remember that a proof can only exists within the duration of the transaction. The proof is then dropped at the end of the transaction. Therefore, the proof does not need to be in a resource container.
Full Implementation
That’s the essence of application-level auth. It has the benefit of allowing us to write custom validations. With that in mind, here is the full implementation of our XRDDistributor. Things worth mentioning that wasn’t explained in the previous snippets are that in our struct we want to record the
ResourceAddress of the claim_badge so that the component can validate proofs that are passed to our claim_tokens method against it. We also have a method to mint_claim_badge which allows users to mint their claim_badge that we didn’t show earlier.use scrypto::prelude::*;#[derive(ScryptoSbor, NonFungibleData)]struct ClaimData { #[mutable] claimed: bool}#[blueprint]mod token_giveaway { struct TokenGiveaway { minter_badge_vault: FungibleVault, vault: FungibleVault, claim_badge_manager: ResourceManager, } impl TokenGiveaway { pub fn instantiate_token_giveaway() -> Global<TokenGiveaway> { let minter_badge = ResourceBuilder::new_fungible(OwnerRole::None) .mint_initial_supply(1); let tokens = ResourceBuilder::new_fungible(OwnerRole::None) .mint_initial_supply(1000); let claim_badge = ResourceBuilder::new_ruid_non_fungible::<ClaimData>(OwnerRole::None) .mint_roles(mint_roles!( minter => rule!(require(minter_badge.resource_address())); minter_updater => rule!(deny_all); )) .non_fungible_data_update_roles(non_fungible_data_update_roles!( non_fungible_data_updater => rule!(require(minter_badge.resource_address())); non_fungible_data_updater_updater => rule!(deny_all); )) .create_with_no_initial_supply(); Self { minter_badge_vault: FungibleVault::with_bucket(minter_badge), vault: FungibleVault::with_bucket(tokens), claim_badge_manager: claim_badge, } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } pub fn mint_claim_badge(&mut self) -> NonFungibleBucket { let claim_badge = self.minter_badge_vault.authorize_with_amount(1, || { self.claim_badge_manager .mint_ruid_non_fungible( ClaimData { claimed: false } ) } ).as_non_fungible(); return claim_badge } pub fn claim_tokens(&mut self, claim_badge_proof: NonFungibleProof) -> FungibleBucket { let checked_proof = claim_badge_proof.check_with_message( self.claim_badge_manager.address(), "Incorrect proof!" ); let nft = checked_proof.non_fungible::<ClaimData>(); let nft_data = nft.data(); // Asserting that the claimed field does not have a value of true. assert_ne!( nft_data.claimed, true, "You have already claimed your tokens" ); self.minter_badge_vault.authorize_with_amount(1, || { self.claim_badge_manager.update_non_fungible_data( &nft.local_id(), "xrd_claimed", true ) }); return self.vault.take(10); } }}Line 10: The
ResourceAddress of our claim_badge is recorded in our struct. This is so the component understands how to check the proof that is sent to the component against it.//More on Validating Proofs
Notice when we pass proof directly into the method, we had to call
.check() on the proof. We highly recommend that any proof that is passed as an argument to your method(s) should be validated calling .check() as it ensures that the proof is what you expect it to be.pub fn query_ticket(&mut self, ticket: NonFungibleProof) { // Make sure the provided proof is of the right resource address let validated_ticket = ticket.check(self.ticket_address).expect("Wrong badge provided."); // Get the data associated with the passed NFT proof let non_fungible = validated_ticket.non_fungible::<TicketData>(); let ticket_data = non_fungible.data::<TicketData>(); info!("You inserted {} XRD into this component", ticket_data.xrd_amount);}Line 4: Here we authenticate the proof that was passed to this method.Line 7-8: Only after the proof is authenticated can we view its non-fungible content.
Line 10: We use
Proofs passed directly to the component starts as an “unchecked” proof, meaning that the proof has not yet been validated. This limits you from viewing the content of the proof. Meaning, if the proof was of a non-fungible type, then the
Line 10: We use
xrd_amount within the NonFungibleData to inform the user the amount of XRD recorded in this non-fungible resource. Proofs passed directly to the component starts as an “unchecked” proof, meaning that the proof has not yet been validated. This limits you from viewing the content of the proof. Meaning, if the proof was of a non-fungible type, then the
NonFungibleData can’t be retrieved until the proof is “checked”. Checking a proof is essentially comparing the ResourceAddress of the underlying proof with what you expect the ResourceAddress to be. Afterwards, you may continue to write additional custom validations or retrieve the NonFungibleData from the proof.//Components Generating Proofs
Our examples have shown callers requiring proofs to access a component’s permissioned method. However, component’s may also need access to a permissioned method as well. We might have a component which we may want to “airdrop” tokens to accounts. However, with the way accounts work on Radix, this may prove to be challenging as users have an ability to reject tokens being randomly sent to their accounts. As a result, components that wish to regularly send tokens to an account will require the component to become an “authorized depositor”, which generates a badge for the component to keep and require the component to present this badge every time the component wishes to send tokens to an account. Presenting this badge will be in a form of a proof.
pub fn send_airdrop(&mut self, account: Global<Account>) { self.authorized_badge_vault.authorize_with_amount(dec!(1), || { let bucket_of_tokens = self.airdrop_vault.take(1); account.try_deposit_or_abort( bucket_of_tokens, Some(ResourceOrNonFungible::Resource(self.authorized_badge_vault.resource_address())) ); });}Line 1: As we saw in Chapter 2.3: Anatomy of a Blueprint, globalizing a component returns a strongly typed
Line 2: Calling
Line 8: The
Within Scrypto, proofs can be created from a resource contained within a vault or (rarely) a bucket. If a component wishes to generate a proof of a badge it contains for a transaction, they may do so and pass it up to the AuthZone for validation. In this context, when the component is sending a transaction to the account to send tokens, we call on authorized_with_amount to handle this validation automatically.
ComponentAddress as a Global, since accounts on Radix are native, we can easily reference Global, ensuring that the ComponentAddress passed in is, in fact, a native account. Line 2: Calling
authorized_with_amount on a vault will automatically handle proof generation and passing for the component to access a permissioned method.Line 8: The
try_deposit_or_abort method has an optional argument where an authorized depositor badge is passed in to validate that the required badge needed to deposit tokens to the account.Within Scrypto, proofs can be created from a resource contained within a vault or (rarely) a bucket. If a component wishes to generate a proof of a badge it contains for a transaction, they may do so and pass it up to the AuthZone for validation. In this context, when the component is sending a transaction to the account to send tokens, we call on authorized_with_amount to handle this validation automatically.
//Applying the Approaches to Authenticate Proofs
Now that you are equipped with conceptual understanding the two authentication models in Scrypto. It’s worth knowing that while these two approaches have varying benefits, it's important to know that we can use both authentication systems conjointly. The nuance here is that we should use these approaches one after another. To expand, we may have a set of methods which requires the
NonFungibleData of the proof and another set of methods that does not. It is then advisable to use application-level auth for methods that require the NonFungibleData of the Proof and system-level auth for methods that do not. Wherever possible, it is strongly advised to use system-level auth as it provides stronger security around authenticating proofs.