It's checked in ConnectInputs:
// If prev is coinbase, check that it's matured
if (txPrev.IsCoinBase())
for (CBlockIndex* pindex = pindexBlock; pindex && pindexBlock->nHeight - pindex->nHeight < COINBASE_MATURITY; pindex = pindex->pprev)
if (pindex->nBlockPos == txindex.pos.nBlockPos && pindex->nFile == txindex.pos.nFile)
return error("ConnectInputs() : tried to spend coinbase at depth %d", pindexBlock->nHeight - pindex->nHeight);
You'll be familiar with maturation if you've ever generated solo (or on Eligius): the client lists those coinbase transactions as "Generated (matures in x more blocks)".
The UI also offers this help text about maturation:
Generated coins must wait 120 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, it will change to "not accepted" and not be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.